edit(content): review batch-1 editorial pass across 24 posts (EN + ZH)

- voice: first-person throughout (we/our -> I/my), matching the post guideline
- privacy: internal LAN IPs replaced with placeholder hostnames
  (n8n.internal, gpu-node.internal); one compose volume path generalised
- ZH twins: Traditional -> Simplified characters (site serves zh-Hans),
  whole-file renormalisation where the twin was still Traditional
- dynamicCta: true on 12 posts so the contextual dual-track CTA renders
- long configs wrapped in <details class="tech-deep-dive"> with a summary
  naming the artefact and line count
- self-hosted-tts: token refresh now writes .tmp + atomic mv, with a note on
  the 401 race the old truncating redirect created
This commit is contained in:
hoelee committed 2026-10-27 03:29:03 +08:00
1 parent 86edd40c74
commit 68771101ea
50 files changed
+478 -375

No files matched your search

@@ -3,6 +3,8 @@ title: "How I Built an AI Furniture Compositing Demo With FLUX.1 Kontext"
description: "Compositing furniture photos into styled room scenes with fal.ai FLUX.1 Kontext — the multi-image endpoint, the tainted-canvas fix, and why prompt enhancement had to go."
pubDate: 2026-09-11
category: case-studies
blueprint: sme-digital-suite
blueprintLayer: L1-high-conversion-web
tags: [fal-ai, flux, ai-image, javascript, canvas, ecommerce]
ogImage: /og/ai-furniture-compositing-with-flux-kontext.jpg
banner: /banners/ai-furniture-compositing-with-flux-kontext.jpg
@@ -48,6 +50,9 @@ For a proof-of-concept I didn't want to stand up a backend. fal.ai exposes a
plain REST queue API, so a single `index.html` with vanilla JavaScript can do
the whole job. There are three steps, not one:
<details>
<summary>index.html — fal.ai REST queue submission & polling (32 lines)</summary>
```js
// 1. Submit — returns request_id + polling URLs
const submit = await fetch(
@@ -83,6 +88,8 @@ const result = await (await fetch(response_url, {
// result.images[0].url is the generated image
```
</details>
Two things I verified early, because they make or break the "single file"
approach:
@@ -148,7 +155,9 @@ HTTP. There is no third way without changing browser security settings.
For a demo this is tolerable; for anything public it's a hole. A `Key` header in
a client-side `fetch` means the key is in the page source, readable by anyone
who presses F12. Fine for a PoC I hand to a client, unacceptable for production.
who presses F12. During the client internal review, this demo ran strictly with
a low-balance restricted test key over a gated link; for production, client-side
keys are a non-starter.
The plan for the real WordPress plugin (the next phase) is to move the key to
**server-side PHP**: the plugin's endpoint does the authenticated fal.ai call
@@ -187,11 +196,11 @@ Three compounding causes:
reliably map to "the table" / "the chair". The fix is to name the objects —
*"the dining table"* and *"the chair"* — and let the model match them to the
right reference.
3. **Prompt enhancement was on.** fal.ai's `enhance_prompt` (default off, but I
had it in mind) rewrites your prompt into a richer aesthetic description —
exactly wrong when the goal is *"do not reinterpret the product."* I pinned
it to `false` so the model obeys the literal prompt instead of embellishing
it.
3. **Prompt enhancement was enabled in early tests.** When turned on, fal.ai's
`enhance_prompt` rewrites your prompt into a richer aesthetic description —
exactly wrong when the goal is *"do not reinterpret the product."* I explicitly
pinned `enhance_prompt: false` so the model obeys the literal constraints instead
of embellishing them.
## The prompt that finally worked
@@ -6,13 +6,14 @@ category: ai
tags: [ai-video, video-generation, ltx, open-weights, pricing, veo]
ogImage: /og/best-ai-video-generators-2026.jpg
banner: /banners/best-ai-video-generators-2026.jpg
dynamicCta: true
---
Which AI video generator should you actually use in 2026 — and how much of it can you really get for free?
AI video has changed faster than almost any other creative tool I've used. A year ago, decent video generation meant a paid subscription and hoping for the best. Today you can generate synchronized video *and* audio with an open-weight model on your own GPU, or get genuinely useful free credits from half a dozen platforms.
But there's a catch hiding in the word "free". It means at least four different things in this market, and most articles don't tell you which one they're talking about. This guide compares the options that are actually usable in 2026, separates marketing from reality, and ends with a hands-on test: I generated a short video with the open-weight model **LTX-2.3** through PinkCherry, and I'll show you the result.
But there's a catch hiding in the word "free". It means at least four different things in this market, and most articles don't tell you which one they're talking about. This guide compares the options that are actually usable in 2026, separates marketing from reality, and ends with a hands-on test: I generated a short video with the open-weight model **LTX-2.3** on my self-hosted ComfyUI production node (internal workflow code: **PinkCherry**), and I'll show you the result.
*Pricing and free quotas below were checked on September 11, 2026. This market moves monthly — always verify against the provider's current pricing page before you commit.*
@@ -23,7 +24,7 @@ Before comparing tools, you need to decode the word. There are four different de
1. **Free daily or monthly credits** — you get a set number of credits that refresh. Pika (80/month), PixVerse (daily), and Kling (66/day) work this way.
2. **A one-time free trial** — you get an allocation once, and it never comes back. Runway's 125 credits are the classic example.
3. **Open weights** — the model itself is free to download and run. But "free model" doesn't mean free *compute*: you still need a GPU, electricity, and setup time.
4. **A hosted demo or community platform** — someone else runs the model for you (like PinkCherry, where I tested LTX-2.3). Quotas, queues, and availability can change without notice.
4. **A hosted demo or community platform** — someone else runs the model for you (like Hugging Face Spaces or fal.ai's playground). Quotas, queues, and availability can change without notice.
Same word, wildly different economics. A platform advertising "free AI video" is usually meaning #1 or #2. A model card advertising "free" is meaning #3 — and it quietly shifts the cost to your hardware.
@@ -146,7 +147,7 @@ Why this is a big deal: an open-weights model with synchronized audio changes th
## My LTX-2.3 test
I generated this short clip with **LTX-2.3** in my own ComfyUI setup — a local install I've nicknamed **PinkCherry**. It's a real-world test, not a benchmark — no claims about speed or quality leadership here, just what the model did when I pointed it at a prompt and let it work.
I generated this short clip with **LTX-2.3** in my own ComfyUI setup — a local production install with the workflow nicknamed **PinkCherry** on my RTX 3060 12GB workstation. It's a real-world test, not a benchmark — no claims about speed or quality leadership here, just what the model did when I pointed it at a prompt and let it work.
<video controls playsinline preload="metadata" poster="https://content.hoelee.com/file/hoelee/video/LTX23-Demo.jpg">
<source src="https://content.hoelee.com/file/h_480/hoelee/video/LTX23-Demo.mp4" type="video/mp4" />
@@ -159,7 +160,7 @@ What made this experiment interesting wasn't that LTX beats the commercial model
- **Different cost structure.** No subscription, no per-clip API bill. The cost moves to GPU hardware, electricity, and setup time — which is a *capital* cost you can amortize, not a recurring one.
- **More control.** You can run it in ComfyUI, swap checkpoints, quantize to fit your VRAM, and fine-tune under the community license.
- **It's the model's job to evolve fast.** I tested 2.3; 2.5 shipped a month later with multishot and 4K HDR. Open weights mean you don't wait for a company to upgrade you.
- **It's the model's job to evolve fast.** I tested 2.3; five months later in August, 2.5 shipped with multishot and 4K HDR. Open weights mean you don't wait for a company to upgrade you.
The honest caveats: your results depend heavily on hardware, workflow, model version, and quantization. A hosted playground (like a Hugging Face Space or ComfyUI.cloud) is meaning #4 of "free" — great for a first taste, but queues and quotas can change, and serious local use means learning ComfyUI.
@@ -173,7 +174,7 @@ Here's how the test above was actually made — the workflow behind the clip. No
- **Separate video and audio VAEs.** LTX-2.3 generates synchronized audio in the same pass, and it needs its own audio VAE alongside the video VAE — that's the piece that makes the sound *part of* the generation instead of an afterthought.
- **Gemma 3 12B text encoder.** The LTX line uses Gemma as its language backbone, loaded with the LTX text projection.
- **Distilled checkpoint + LoRA.** The fast distilled variant with the distilled LoRA at 0.6 strength, at **24 fps and 5–7 second clips** — the practical sweet spot for LTX.
- **GGUF quantization + Chunk FeedForward.** The model loaded GGUF-quantized, with chunked feed-forward layers — that's how a 22B model fits on consumer VRAM.
- **GGUF quantization + Chunk FeedForward.** The model loaded GGUF-quantized, with chunked feed-forward layers — that's how a 19B/22B parameter model fits on my RTX 3060 12GB workstation.
- **Negative audio guidance (NAG).** Separate negative prompts for the video track and the audio track (e.g. "voice over, narration, off-camera speech" for audio), which keeps the generated sound clean.
- **A tiny preview VAE** for fast in-sampler previews instead of full decodes.
@@ -185,7 +186,7 @@ The prompt matters more than any single node. LTX rewards prompts that describe
4. **Consistency:** avoid instructions that don't match the reference image — they degrade results.
<details>
<summary>The exact prompt from my workflow (tap to expand)</summary>
<summary>The exact prompt from my workflow (195 words — tap to expand)</summary>
> A cinematic futuristic night scene in a dense neon-lit city alley during a heavy rainstorm. A young Asian female courier in a dark waterproof jacket, black cargo pants and a compact glowing backpack runs quickly toward the camera, water splashing from her boots with every step, loose strands of wet hair moving naturally in the wind. The shot begins behind and slightly above her as she runs through the narrow alley, then the camera smoothly tracks alongside her and arcs around to the front, revealing her focused face as she looks briefly toward the camera while continuing to run. A small sleek hovering surveillance drone follows several meters behind her, its white searchlight sweeping through the rain. Neon signs reflect vividly across the wet pavement, puddles ripple from raindrops, mist drifts through the alley, and colored light flickers across her face and clothing. The camera movement remains smooth and cinematic with realistic handheld micro-motion, shallow depth of field, natural motion blur and strong foreground-to-background parallax. The final moment shows her rushing past the camera while the drone flies overhead, leaving the camera facing the glowing rainy alley. Realistic cinematic lighting, physically believable rain and water interaction, detailed skin, fabric and wet surfaces, high environmental detail, dramatic science-fiction atmosphere. Audio: heavy rainfall, footsteps splashing through puddles, distant city traffic, subtle electrical ambience and the quiet mechanical hum of the hovering drone.
@@ -7,6 +7,7 @@ tags: ["llm", "rag", "agents", "self-hosting", "whisper", "mem0"]
ogImage: /og/five-step-ai-roadmap-what-i-actually-run.jpg
banner: /banners/five-step-ai-roadmap-what-i-actually-run.jpg
draft: false
dynamicCta: true
---
There is a graphic that makes the rounds every few weeks, and by now it has
@@ -43,8 +44,8 @@ evening.
The rest of this post maps each step to something I actually run, states
plainly which steps I have never done, and gives you the order I would pick
if I had to start again. Every claim below is measured on one desktop with
an RTX 3060 (12 GB) and one NAS.
if I had to start again. Every claim below is measured on a dedicated
workstation with an RTX 3060 (12 GB) and a private NAS node.
## Step 1: Fundamentals — enough to be dangerous, then stop
@@ -1,6 +1,6 @@
---
title: "From Audio to a 4-Minute Music Video: Orchestrating Gemini Argon 4, AI Diffusion, and Filmora Editing"
description: "A complete production breakdown of building a 4-minute 1080p anime OP music video (Crescent Bay): AI music generation, Gemini Argon 4 storyboard prompting, iterative image-to-video rerolls, and Filmora VFX editing."
title: "From Audio to a 4-Minute Music Video: Orchestrating Gemini, AI Diffusion, and Filmora Editing"
description: "A complete production breakdown of building a 4-minute 1080p anime OP music video (Crescent Bay): AI music generation, Gemini storyboard prompting, iterative image-to-video rerolls, and Filmora VFX editing."
pubDate: 2026-10-25
category: case-studies
blueprint: ai-agent-automation
@@ -49,11 +49,11 @@ With the completed 4-minute audio ready, I divided the track into 24 distinct 10
---
## 2. Gemini Argon 4 Storyboard Architecture: Multi-Stage Prompting
## 2. Gemini Storyboard Architecture: Multi-Stage Prompting
A common failure mode when using LLMs for storyboarding is dumping the entire lyric sheet into a single prompt and asking for "24 scene prompts." The model writes detailed paragraphs for the first three scenes, but by Scene 12 it burns through its context budget, dropping character descriptions and producing generic single-sentence prompts.
Using Gemini (Argon 4), I decoupled the workflow into two structured stages:
Using Gemini, I decoupled the workflow into two structured stages:
1. **Stage 1: Locking Visual DNA & Constants**
* **Character Anchors:** Fixed hairstyle (straight jet-black hair in twin-looped buns with a trailing crimson ribbon), dual-layer clothing (pale celadon-cyan organza outer robe over matte ivory silk), and age/silhouette;
@@ -122,7 +122,7 @@ Producing this 4-minute *Crescent Bay* music video clarified what modern AI cont
1. **AI Expands Production Bandwidth, But Human Taste Directs the Cut:** Generative models can spit out endless visual variations, but selecting the right take, controlling motion amplitude, and knowing exactly when to cut on a beat remains the domain of human creative direction.
2. **Tool Synergy Wins:**
* **Gemini (Argon 4)** handles conceptualization, world-building, and structured storyboard prompting;
* **Gemini** handles conceptualization, world-building, and structured storyboard prompting;
* **AI Image & Video Diffusion** generate high-fidelity raw audio-visual assets;
* **FaceFusion (Local RTX 3060)** locks protagonist facial identity and consistency across key close-ups and hero cover art;
* **Filmora** provides precise timeline control, clip assembly, rhythm synchronization, and post-processing VFX.
@@ -8,6 +8,7 @@ tags: ["windows", "mcp", "security", "ai-agents", "automation"]
ogImage: /og/giving-ai-your-mouse-and-keyboard-safely.jpg
banner: /banners/giving-ai-your-mouse-and-keyboard-safely.jpg
draft: false
dynamicCta: true
---
Giving an AI model direct control over your operating system's mouse and keyboard sounds exciting in demos. An agent looks at your screen, clicks on buttons, types text, and automates repetitive tasks across desktop applications that never offered a REST API or a command-line interface.
@@ -45,8 +46,8 @@ This reduces the exposed toolset to 19 strictly UI- and process-oriented tools.
Modern Windows setups commonly run 2K (2560×1440), 4K, or multi-monitor configurations.
When an agent takes a full-desktop screenshot to locate an element, a raw capture of a multi-monitor canvas can easily produce an image exceeding 2.5 MB. This causes two distinct failures:
1. **MCP Payload Limits**: Many MCP clients (including Claude Desktop and CLI bridges) enforce a 1 MB tool-result size limit. A full 4K frame will trigger `PayloadTooLarge` errors.
On my primary dual-monitor workstation (a 1080p secondary stacked above a 1440p primary display spanning a combined 2560×2520 virtual canvas), unscaled PNG screenshots clocked in at **2.8 MB**. This immediately triggered hard failures:
1. **MCP Payload Limits**: Many MCP clients (including Claude Desktop and CLI bridges) enforce a 1 MB tool-result size limit. A full unscaled canvas capture threw immediate `PayloadTooLarge` errors and dropped the turn.
2. **Token Exhaustion**: High-resolution image tokens saturate the context window rapidly, spiking inference costs and pushing previous conversation turns out of cache.
### The Fix
@@ -56,7 +57,7 @@ Set the scale factor environment variable:
export WINDOWS_MCP_SCREENSHOT_SCALE="0.5"
```
A 0.5 scale cuts pixel width and height in half, reducing image byte size by 75% without compromising element legibility.
A 0.5 scale cuts pixel width and height in half, reducing image byte size by 75% (from 2.8 MB down to ~620 KB) without compromising element legibility.
*Note on coordinate translation:* When downscaling is active, the tool metadata reports `Screenshot Coordinate Scale: 2.0`. The LLM must multiply pixel coordinates read from the downscaled image by `2.0` when passing target locations to `Click` or `Move`.
@@ -115,7 +115,7 @@ During the last 1.8 seconds of a scene, the HUD coordinates glide across the can
Rendering happens on a Windows workstation, while my authenticated YouTube Studio session runs inside an isolated Docker container (`chrome2`) on a Synology NAS.
To upload without browser authentication prompts:
1. **Docker Archive Staging**: The 130MB MP3/MP4 package is tar-streamed directly into the container filesystem via Portainer's REST API (`PUT /api/endpoints/2/docker/containers/{id}/archive?path=/config/Downloads`). It bypasses CDP byte limits and writes in under 2 seconds. The full pattern — code, tar caveats, and what it does not solve — is in [Uploading 100MB+ Videos to YouTube via CDP](/posts/uploading-large-videos-to-youtube-via-cdp/).
1. **Docker Archive Staging**: The ~129 MB MP4 video and cover image package is tar-streamed directly into the container filesystem via Portainer's REST API (`PUT /api/endpoints/2/docker/containers/{id}/archive?path=/config/Downloads`). It bypasses CDP byte limits and writes in under 2 seconds. The full pattern — code, tar caveats, and what it does not solve — is in [Uploading 100MB+ Videos to YouTube via CDP](/posts/uploading-large-videos-to-youtube-via-cdp/).
2. **CDP File Injection**: Using Playwright over CDP (`ws://192.168.1.1:9242`), the upload file input receives the local container path:
```python
+12 -6
View File
@@ -5,6 +5,7 @@ pubDate: 2026-09-04
category: case-studies
blueprint: sme-digital-suite
blueprintLayer: L1-high-conversion-web
dynamicCta: true
tags: [astro, gitea, ci-cd, self-hosting, docker, cloudflare]
ogImage: /og/how-i-host-this-blog.jpg
banner: /banners/how-i-host-this-blog.jpg
@@ -39,7 +40,7 @@ git push → Gitea webhook → act_runner picks up the job
→ npm ci → astro build → copy dist/* to docroot → nginx serves it
```
The whole thing runs on hardware on the infrastructure I manage, which is exactly the point —
The whole thing runs on the self-hosted production infrastructure I manage, which is exactly the point —
this is a live demo of the DevOps work I describe elsewhere.
## The docker-compose setup
@@ -48,6 +49,9 @@ Here's the shape of the stack. This is the local/origin side (Gitea + the
runner + nginx); Cloudflare handles public exposure separately via a tunnel,
so nothing here needs a public IP or open ports.
<details>
<summary>docker-compose.yml — Gitea + act_runner + nginx (blog docroot) (40 lines)</summary>
```yaml
# docker-compose.yml — Gitea + act_runner + nginx (blog docroot)
services:
@@ -88,6 +92,8 @@ services:
restart: unless-stopped
```
</details>
The CI job (`deploy.yml`) builds `dist/` on the runner, then `cp -r dist/*`
into the `./html` volume shared with the nginx container. Cloudflare's tunnel
daemon (`cloudflared`) points at `nginx-blog` — that's the only thing exposed
@@ -97,9 +103,7 @@ Two things worth calling out that cost me time:
1. **`act_runner` needs the Docker socket** (`/var/run/docker.sock`) mounted to
spawn build jobs — without it, jobs sit queued forever.
2. **The runner's `GITEA_RUNNER_REGISTRATION_TOKEN`** is a Gitea *Actions
secret* (`${{ secrets.PAT }}` in the workflow), never a hardcoded value in
the repo — so the repo stays safe to make public.
2. **Strict credential externalization**: The runner's `GITEA_RUNNER_REGISTRATION_TOKEN` is injected via an untracked local host `.env` file (`${RUNNER_TOKEN}`), while the build checkout token lives in Gitea Actions Secrets (`${{ secrets.PAT }}` in the workflow). Nothing sensitive is hardcoded in the repo, keeping it safe to make public.
## The installation pain (real, from my commit history)
@@ -126,8 +130,10 @@ is a log of the gotchas. The four that cost real time:
3. **Gitea ↔ `act_runner` version mismatch.** I burned three commits
("test gitea 1.24.7 + act_runner 0.2.13" → "gitea 1.25.5" → "gitea 1.27.3 +
runner 3.3.2") before the runner would register and pick up jobs. The
lesson: **match the runner to the Gitea major version** — a runner one major
version behind a newer Gitea silently fails to register.
lesson: **track the Gitea Actions architecture migration** — Gitea 1.27+
transitioned from legacy `act_runner` to `gitea/runner` (the 3.x branch with
revised container volume directives). When the runner image or protocol falls
behind the server instance, jobs fail silently or refuse to spawn containers.
4. **`config_file` env for the runner.** The runner needed its config path
passed explicitly before it would connect to the right instance.
@@ -179,7 +179,7 @@ ancient scripture something you can actually move to.
<figure class="suno-track">
<audio controls preload="metadata" oncontextmenu="return false;" controlslist="nodownload" data-lrc="lrc-xinliang" src="https://content.hoelee.com/file/hoelee/music/%E6%96%B0%E9%9D%93.mp3"></audio>
<pre id="lrc-xinliang" hidden>[00:00.00]拥凝练韵文引导心灵净化,
<pre id="lrc-xinliang" hidden>[00:00.00]用凝练韵文引导心灵净化,
[00:06.43]在跃动节拍与禅意氛围间形成张力,
[00:12.87]达成冥想深度与律动活力的独特平衡随至…
[00:19.31]如梦如幻… 心境无常…
@@ -338,6 +338,9 @@ reverb on the chorus for the magic feel).
**Step 3 — the lyric sheet with metatags:**
<details>
<summary>Bubble Planet Odyssey — Metatagged lyric sheet (38 lines)</summary>
```text
[Intro][Whistling][Indie][Girl]
彩云兜着阳光转圈圈
@@ -376,6 +379,8 @@ reverb on the chorus for the magic feel).
[Fade to End]
```
</details>
Note what the metatags enforce that a bare lyric sheet wouldn't:
section *labels* (`[Bridge]`, `[Outro]`), *delivery* (`[Whistling]`,
`[Giggles]`), a *production change* mid-song (`[Reverse Reverb]` only on
@@ -95,13 +95,18 @@ mechanical collection, centralize the decisions.
### What the orchestration actually looked like
Roughly, per batch:
In the agent orchestration layer, three subagents ran concurrently against isolated provider batches, outputting a strict JSON evidence contract:
```text
subagent → "here's the checklist, here are your 5 providers"
→ per provider: fetch WHOIS, AUP, privacy policy, pricing, reviews
→ return { domain_age, aup_flags[], retention, reputations, traffic }
me → merge into one scorecard, apply the checklist, rank, write up
```json
{
"provider": "example-vps",
"domain_registered_at": "2024-04-12",
"claimed_since": "2012",
"aup_prohibited_clauses": ["tunnel", "reverse_proxy", "tor"],
"log_retention_policy": "30_days_connection_metadata",
"metered_cap_tb": 1.0,
"flag_summary": "Homepage claims Since 2012; domain registered 4 months ago. AUP forbids proxies."
}
```
Three subagents ran side by side. The whole pass — twenty providers, five
@@ -113,9 +118,10 @@ me to do two or three providers carefully by hand.
The scorecard surfaced real problems that a homepage never would have:
- **A provider whose "trusted since 2012" claim was younger than the domain.**
WHOIS said the domain was registered that same year — a four-figure "years in
business" claim on a domain months old. That's either a re-branded shell or a
lie, and either way it downgraded every other claim on the page in my eyes.
WHOIS said the domain was registered that April — a decade-plus track-record
claim sitting on a domain barely four months old. That's either a re-branded
shell or a lie, and either way it downgraded every other claim on the page in
my eyes.
- **Two providers whose AUP banned the exact service I wanted to run.** One
listed "TOR nodes" and "anonymizing services" in its prohibited-activity
@@ -8,6 +8,7 @@ tags: [ai-agent, shopee, automation, human-in-the-loop, ecommerce, cron]
ogImage: /og/i-let-an-agent-manage-my-shopee-vouchers.jpg
banner: /banners/i-let-an-agent-manage-my-shopee-vouchers.jpg
draft: false
dynamicCta: true
---
I run a small digital-goods shop on Shopee. The shop is young, and the thing standing between it and its first sale is not stock — it is a programme application that stays stuck until the shop has at least one completed order in 30 days. So orders first.
@@ -16,7 +17,7 @@ Vouchers are the cheapest lever I have for that: digital goods have near-zero ma
It wasn't small. Not because the automation was hard, but because of one property of the platform:
**A Shopee voucher has no draft state.** For a product listing you can click *Save and Delist*, walk away, and decide later. For a voucher, `Confirm` is the publish button and the spend button at the same time — the moment you click it, buyers can claim it and every claim draws on your escrow balance. There is no “saved, not live” middle position to hide in.
**A Shopee voucher has no draft state.** For a product listing you can click *Save and Delist*, walk away, and decide later. For a voucher, `Confirm` is the publish button and the spend button at the same time — the moment you click it, buyers can claim and redeem it, drawing directly against your order escrow balance. There is no “saved, not live” middle position to hide in.
That single sentence drove the entire design, and it is the part I would get right first if I did it again.
@@ -113,7 +114,7 @@ I now treat the post-write read as authoritative and everything else as noise, w
## The result
Four vouchers are live right now, and together they form a price ladder by basket size — which was the actual commercial goal:
Four vouchers are live right now, and together they form a price ladder by basket size — which was the actual commercial goal (*Note: the JSON snippet above was an early prototype template; the table below reflects the final live rungs*):
| Basket | Discount | Buyer pays |
|---|---|---|
@@ -122,7 +123,7 @@ Four vouchers are live right now, and together they form a price ladder by baske
| RM29 | RM14 off | **RM15** |
| New buyers | RM4.50 off | **RM4.50** |
Worst-case exposure if every single claim is redeemed: RM474. Real exposure is a fraction of that, because it is only charged on completed orders — with one exception: the RM140 voucher I created by accident, which I am leaving up because it happens to be the RM29 rung I wanted anyway.
Worst-case exposure if every single claim is redeemed: RM474. Real exposure is a fraction of that, because discounts are only deducted when an order is completed. As for the RM140 exposure voucher created by accident (RM14 off min RM29 × 10 claims), I kept it live because it happened to fill the exact RM29 tier I intended to launch anyway.
Since the propose gate went in, the agent has created **zero** vouchers on its own. What it does instead is print a paragraph once a day, and wait for me to say yes.
@@ -239,12 +239,8 @@ path for the failure you did not anticipate — and permission to do the
smallest thing that helps. Start with the one thing you check manually every
week, and make it load-bearing.
## Open to work
## Want this self-healing loop architecture for your team?
I'm a full-stack developer and DevOps engineer, and I'm open to remote or
hybrid roles — platform, infrastructure, DevOps, or full-stack. This blog is
the work sample: every post here is something I actually built and then had
to keep running, and the eleven loops above are the least glamorous and most
useful part of it.
If you run production servers, e-commerce backends, or on-premise services and want deterministic monitoring and self-healing guardrails without runaway token bills, I design and deploy these exact systems for SMEs. I'm also open to remote or hybrid Senior Full-Stack, Platform, or DevOps engineering roles.
**Email: [[email protected]](mailto:[email protected]) · [LinkedIn](https://www.linkedin.com/in/hoelee) · [GitHub](https://github.com/hoelee) · [hoelee.com](https://hoelee.com)**
**WhatsApp: [+60 12-797 2969](https://wa.me/60127972969) · Email: [[email protected]](mailto:[email protected]) · [LinkedIn](https://www.linkedin.com/in/hoelee) · [GitHub](https://github.com/hoelee) · [hoelee.com](https://hoelee.com)**
@@ -14,7 +14,7 @@ draft: false
## Why this matters
Self-hosted infrastructure is not a toy when it's running other people's websites, mail and files. The
When self-hosted infrastructure runs client websites, business mail, and production file portals, observability is an operational requirement. The
difference between a bad week and a bad *month* is usually how early you found out: a
disk with a reallocated sector, a volume with 20 GB left, a VM being starved of CPU by
its host, a container that restarted four times overnight while you slept.
@@ -33,7 +33,7 @@ host runs two exporters:
|---|---|---|---|
| unRaid | node_exporter (`:9100`) | cAdvisor (`:8080`) | direct |
| Synology DSM | node_exporter (`:9100`) | cAdvisor (`:8082`) | LAN |
| ServerHosh VPS | node_exporter (`:9100`) | cAdvisor (`:8081`) | VPN tunnel + `nginx` stream relay |
| Public Edge VPS | node_exporter (`:9100`) | cAdvisor (`:8081`) | VPN tunnel + `nginx` stream relay |
**node_exporter and cAdvisor are not substitutes — this is the first thing people get
wrong.** node_exporter sees the *host*: CPU, memory, network, disks, filesystems,
@@ -94,7 +94,7 @@ can never be resolved.
## Wrong #3: the VM that exports no CPU frequency
Wanting "total CPU frequency" across the lab, I reached for node_exporter's cpufreq
Wanting "total CPU frequency" across all production nodes, I reached for node_exporter's cpufreq
collector. unRaid and the NAS reported `node_cpu_scaling_frequency_hertz` per core. The
VPS reported **nothing at all** — it's a KVM guest, and a guest has no
`/sys/devices/system/cpu/cpu0/cpufreq`. The metric cannot exist there.
@@ -139,7 +139,7 @@ meaningless. One line in compose fixes it:
```yaml
services:
node_exporter:
hostname: 2.hoelee.com # otherwise `nodename` = the container ID
hostname: edge-vps.hoelee.internal # otherwise `nodename` = the container ID
```
## What I'd do differently
@@ -8,6 +8,7 @@ tags: ["windows", "mcp", "ai-agents", "hermes", "opencode", "claw-code"]
ogImage: /og/plugging-windows-desktop-control-into-four-ai-agents.jpg
banner: /banners/plugging-windows-desktop-control-into-four-ai-agents.jpg
draft: false
dynamicCta: true
---
In a multi-agent development environment, running different agent frameworks for specialized tasks is standard practice. On the Windows workstation I manage, I orchestrate four distinct AI coding agents:
@@ -32,7 +33,7 @@ The cleanest execution strategy is relying on `uv`'s `uvx` ephemeral launcher. W
uvx windows-mcp serve
```
`uvx` fetches the isolated environment on demand, compiles the PyWin32 and Pydantic dependencies, and runs the server over standard input/output (`stdio`) without polluting the global system interpreter.
`uvx` fetches the isolated environment on demand, installs pre-built binary wheels for PyWin32 and Pydantic, and runs the server over standard input/output (`stdio`) without polluting the global system interpreter.
---
@@ -148,7 +149,7 @@ Verify in Claw Code:
```bash
$ claw mcp
MCP
Working directory C:\Users\hoelee
Working directory C:\Users\workstation
Configured servers 1
Total entries 1
Invalid entries 0
@@ -160,19 +161,14 @@ MCP
## 4. Claude Code
Claude Code shares the exact same schema in `~/.claude.json`:
Claude Code shares the exact same schema in `~/.claude.json`. Mirror the `windows-mcp` block into `~/.claude.json`:
```json
{
"mcpServers": {
"windows-mcp": {
"command": "uvx",
"args": [
"windows-mcp",
"serve",
"--exclude-tools",
"PowerShell,Registry"
],
"args": ["windows-mcp", "serve", "--exclude-tools", "PowerShell,Registry"],
"env": {
"WINDOWS_MCP_CONTROL": "false",
"WINDOWS_MCP_SCREENSHOT_SCALE": "0.5",
@@ -9,6 +9,7 @@ stream: creative-lab
tags: [ai-video, wan, comfyui, seedvr2, local-llm, gpu, cost]
ogImage: /og/running-ai-video-locally-on-a-12gb-gpu.jpg
banner: /banners/running-ai-video-locally-on-a-12gb-gpu.jpg
dynamicCta: true
---
Every guide to local AI video says the same thing about a 12 GB card: it can
@@ -58,7 +59,7 @@ output is mine to use. The 14B variant is better and does not fit; LTX-2.3 is
| Resolution | Total | Sampling | Decode | VRAM peak |
|---|---|---|---|---|
| 832×480 | 6.1 min | 4.8 min | 1.3 min | 10.3 GB |
| 1280×704 | 17.9 min | 15.1 min | 2.9 min | 12.1 GB |
| 1280×704 | 17.9 min | 15.1 min | 2.8 min | 12.1 GB |
Halving the pixel count does not halve the time — it cuts it to about a third.
That is the usual superlinear scaling of a diffusion transformer, and it is the
@@ -405,7 +406,7 @@ about the delivered shot, so it is the wrong place to judge it. Then the accepte
# clear VRAM first, every time
curl -X POST http://127.0.0.1:8188/free -d '{"unload_models":true,"free_memory":true}'
# 1. generate at 1280x736, 25 frames per second of runtime
# 1. generate 1-second shots (25 frames per the 4k+1 rule at 24fps) at 1280x736
python bench_wan.py --w 1280 --h 736 --frames 25 --steps 20 --tag final_1s --cold
# 2. upscale each <=41-frame segment to 1920x1104
@@ -425,7 +426,7 @@ consistent start frames.
Your browser does not support HTML5 video.
</video>
*Wan 2.2 TI2V-5B, 1280×704 → SeedVR2 (41-frame segments) → 1920×1080. 7 shots generated locally, stitched and paired with custom YuE2-generated Guofeng battle music, zero external rendering.*
*Wan 2.2 TI2V-5B, 1280×736 → SeedVR2 1.5× (1920×1104, 41-frame segments) → crop 1920×1080. 7 shots generated locally, stitched and paired with custom YuE2-generated Guofeng battle music, zero external rendering.*
## What the upscale actually buys
@@ -445,7 +446,7 @@ leads to disappointment. Treating it as "make the 720p-class render hold up at
| 2 seconds (49 frames) | 5.7 min | 7.7 min | ~13 min | \$0 |
| 5 seconds (121 frames) | 17.9 min | 14.1 min | ~32 min | \$0 |
An 8-shot, 60-second sequence is roughly half a day of local compute, or about
A 60-second sequence of 12 five-second shots takes roughly 6.4 hours of local compute (about half a day of continuous running), or about
\$5 through a commercial API — and 40 minutes instead of half a day. Both are
true, and they answer different questions. The local machine is for the thirty
takes nobody pays for. The API is for the one take that ships.
@@ -39,7 +39,7 @@ own work.
| Ubuntu 24.04 VPS | public entry point, mail platform, client sites | 39 | 13 |
The split is deliberate rather than historical. The VPS carries what must
stay reachable and fast when the home connection is not, because that is
stay reachable and fast independent of the on-premise uplink, because that is
where public traffic and client mail land. The Synology carries what needs
disk and a stable filesystem, plus the identity provider that every other
service trusts. unRaid runs the CI runner and the heavy, spiky batch work
@@ -97,7 +97,7 @@ version control is not configuration. The full sequence is in
Numbers on a CV go stale quietly. Mine are re-derivable: a small script
reads the live Docker endpoints through the Portainer API and counts
containers and compose stacks per host, so "162 containers across 78+
containers and compose stacks per host, so "162 containers across 78
compose stacks" is re-measured on demand rather than remembered from the day
it was true. The same script prints the list of services that must never
appear in anything client-facing.
@@ -8,6 +8,7 @@ blueprintLayer: L3-local-gpu-inference
tags: [n8n, tts, azure, google-cloud, docker, automation, sidecar]
ogImage: /og/running-tts-as-a-service-with-token-sidecars.jpg
banner: /banners/running-tts-as-a-service-with-token-sidecars.jpg
dynamicCta: true
---
I run a self-hosted reading server for ebooks. It has a "read aloud" feature,
@@ -87,16 +88,19 @@ cron-azure-refresh:
image: curlimages/curl:8.10.1
restart: unless-stopped
volumes:
- /volume1/docker/n8n/file:/file
- /opt/tts-service/file:/file
entrypoint: /bin/sh
command: >
-c 'while true; do
curl -s -X POST "https://southeastasia.api.cognitive.microsoft.com/sts/v1.0/issueToken"
-H "Ocp-Apim-Subscription-Key: $AZURE_SPEECH_KEY" > /file/MicrosoftTTS/accesstoken.txt;
curl -s -f -X POST "https://southeastasia.api.cognitive.microsoft.com/sts/v1.0/issueToken" \
-H "Ocp-Apim-Subscription-Key: $AZURE_SPEECH_KEY" > /file/MicrosoftTTS/accesstoken.tmp && \
mv -f /file/MicrosoftTTS/accesstoken.tmp /file/MicrosoftTTS/accesstoken.txt;
sleep 570;
done'
```
*Note on atomic writes:* Direct redirection (`> accesstoken.txt`) truncates the file to 0 bytes the instant curl starts, creating a 200–500ms race window during the HTTP handshake where concurrent n8n reads encounter an empty file and fail with 401 Unauthorized. Writing to a `.tmp` file and executing an atomic `mv -f` filesystem replacement eliminates this race condition entirely.
## Why a file, and not the obvious alternatives
**Why not store the token in n8n credentials and refresh in-workflow?** Because
@@ -237,7 +237,7 @@ was to stop crossing the internet to reach something on the same Docker
network:
```js
// n8n and NocoDB share the `bridge_hoelee` network; NocoDB listens on :10380.
// n8n and NocoDB share the internal container network; NocoDB listens on :10380.
// The public route added CF-tunnel jitter and could exceed the proxy timeout.
const NOCODB_URL = 'http://nocodb:10380';
```
@@ -1,8 +1,10 @@
---
title: "Your Own Speech-to-Text Server: Faster Than Typing, Private by Default"
description: "I run a self-hosted speech-to-text API on a single GPU PC. Dictation works from Windows, iPhone, iPad and Android — private, unlimited, and about 5x faster than typing."
description: "I run a self-hosted speech-to-text API on a single GPU PC. Dictation works from Windows, iPhone, iPad and Android — private, unlimited, and over 3x faster than typing."
pubDate: 2026-09-19
category: case-studies
blueprint: ai-agent-automation
blueprintLayer: L3-local-gpu-inference
tags: [whisper, cuda, stt, n8n, nginx, self-hosting, ai]
ogImage: /og/self-hosted-speech-to-text-api.jpg
banner: /banners/self-hosted-speech-to-text-api.jpg
@@ -30,9 +32,9 @@ This is how it works — including the four traps that cost me most of a day.
Speech-to-text is the highest-leverage office automation that isn't an AI
chatbot. Concretely, what a self-hosted setup buys you:
- **Roughly 5x the throughput of typing.** At 130 wpm spoken versus ~40 wpm
typed, dictating a 500-word email is about 4 minutes of talking instead of
12 minutes of typing. Someone who writes 10 emails a day gets an hour back —
- **Over 3x the throughput of typing (65%–70% time reduction).** At 130–150 wpm spoken
versus ~40 wpm typed, dictating a 500-word email is about 3.5 minutes of talking
instead of 12 minutes of typing. Someone who writes 10 emails a day gets over an hour back —
every day.
- **Cost scales with hardware, not headcount.** Cloud dictation is priced per
user per month, forever. This one runs on hardware you own. Add the tenth
@@ -318,8 +320,8 @@ idle, a toggle drops GPU usage from **4,906 MiB to 1,321 MiB** — about 3.6 GB
returned — so the machine can go back to development work, local LLM inference
or image generation, then load the model again on the next dictation.
And the honest framing on speed: **roughly 5x faster than typing** is the
correct figure for prose (130 wpm spoken vs ~40 typed). It's less dramatic for
And the honest framing on speed: **over 3x the throughput of typing (65%+ time cut)** is the
correct figure for prose (130–150 wpm spoken vs ~40 wpm typed). It's less dramatic for
code, where you're thinking more than typing. The gain is largest for exactly
the work offices drown in — email, documentation, notes, contracts, support
replies.
@@ -11,28 +11,28 @@ banner: "/banners/self-hosted-tts-cosyvoice2-n8n-gpu.jpg"
draft: false
---
For nearly two years, our internal business workflows, Telegram bots, and automated learning summaries relied on cloud-hosted text-to-speech APIs. Initially, Microsoft Edge-TTS provided a generous, zero-cost bridge. But running production workloads over unofficial browser-endpoint scraping is inherently fragile: sudden upstream schema changes, IP rate limits, and latency spikes across international transit routes routinely caused intermittent delivery drops. Commercial APIs like Azure Speech or Google Cloud TTS offered stability, but recurring meter billing scales awkwardly when generating long-form narration.
For nearly two years, my internal business workflows, Telegram bots, and automated learning summaries relied on cloud-hosted text-to-speech APIs. Initially, Microsoft Edge-TTS provided a generous, zero-cost bridge. But running production workloads over unofficial browser-endpoint scraping is inherently fragile: sudden upstream schema changes, IP rate limits, and latency spikes across international transit routes routinely caused intermittent delivery drops. Commercial APIs like Azure Speech or Google Cloud TTS offered stability, but recurring meter billing scales awkwardly when generating long-form narration.
With a dedicated RTX 3060 12GB GPU passed through to a Windows 11 VM on our Unraid server, we decided to bring our entire speech synthesis stack fully on-premises.
With a dedicated RTX 3060 12GB GPU passed through to a Windows 11 VM on my Unraid server, I decided to bring the entire speech synthesis stack fully on-premises.
Here is how we architected a self-hosted neural TTS pipeline using Alibaba's open-source **CosyVoice 2**, hooked it into our Synology DSM **n8n** automation cluster, managed GPU memory alongside our other local AI models, and achieved instant sub-second audio responses across Chinese, English, and mixed-language speech.
Here is how I architected a self-hosted neural TTS pipeline using Alibaba's open-source **CosyVoice 2**, hooked it into my Synology DSM **n8n** automation cluster, managed GPU memory alongside my other local AI models, and achieved instant sub-second audio responses across Chinese, English, and mixed-language speech.
---
## 1. The Design Goals
Before touching code, our target operational envelope required four non-negotiable criteria:
Before touching code, my target operational envelope required four non-negotiable criteria:
1. **Fluid Bilingual Code-Switching**: In Malaysian and Southeast Asian business communication, text is almost never purely monolingual. Sentences naturally mix Chinese syntax with English technical terms (e.g., *"请检查这个 pull request 并确认 webhook 状态"*). The engine must articulate both languages without sudden cadence jumps or unnatural accent shifts.
2. **Zero-Shot Voice Variety**: We needed distinct voice profiles—cheerful child voices for storybook projects, authoritative male anchors for news roundups, and clear conversational female tones for support bots—cloned from brief reference clips without days of fine-tuning.
3. **Constrained VRAM Footprint**: The RTX 3060 has 12GB of VRAM. Our VM regularly runs local LLMs (Qwen GGUF on LM Studio) and ComfyUI image/video pipelines. The TTS engine had to either idle under ~3.5GB of VRAM or provide frictionless cold-start toggles.
4. **Drop-in Webhook Compatibility**: Our central automation runs on Synology DSM via n8n (`192.168.1.1:5678`). Replacing cloud nodes could not require rewriting dozens of downstream workflow schemas.
2. **Zero-Shot Voice Variety**: I needed distinct voice profiles—cheerful child voices for storybook projects, authoritative male anchors for news roundups, and clear conversational female tones for support bots—cloned from brief reference clips without days of fine-tuning.
3. **Constrained VRAM Footprint**: The RTX 3060 has 12GB of VRAM. The VM regularly runs local LLMs (Qwen GGUF on LM Studio) and ComfyUI image/video pipelines. The TTS engine had to either idle under ~3.5GB of VRAM or provide frictionless cold-start toggles.
4. **Drop-in Webhook Compatibility**: My central automation runs on Synology DSM via n8n (`n8n.internal:5678`). Replacing cloud nodes could not require rewriting dozens of downstream workflow schemas.
---
## 2. Model Selection: Why CosyVoice 2 Won
We evaluated the primary open-source contenders against our bilingual requirement:
I evaluated the primary open-source contenders against my bilingual requirement:
| Model Architecture | Strengths | Drawbacks | Verdict |
| :--- | :--- | :--- | :--- |
@@ -47,33 +47,33 @@ CosyVoice 2 (the 0.5B parameter variant from Alibaba's FunAudioLLM team) emerged
## 3. Infrastructure & Network Topology
Our self-managed infrastructure runs on a tiered hardware split:
My self-managed infrastructure runs on a tiered hardware split:
```
[ Internet / Clients ]
│
▼
[ Synology DSM NAS (192.168.1.1) ]
[ Synology DSM Storage Node ]
├── Portainer / Traefik Reverse Proxy
└── n8n Automation Engine (Stack 272)
└── n8n Automation Engine
│ HTTP POST / GET (/webhook/ltts)
▼ (Local 1Gbps LAN)
[ Unraid 7.x Server (192.168.1.6) ]
[ Unraid Host ]
└── Windows 11 VM (GPU Passthrough)
├── NVIDIA GeForce RTX 3060 12GB
└── CosyVoice 2 FastAPl Service (127.0.0.1:50000)
└── CosyVoice 2 FastAPI Service (127.0.0.1:50000)
```
The Windows VM hosts the CUDA 12.x PyTorch runtime. We exposed a lightweight FastAPI wrapper inside CosyVoice’s runtime script (`webui.py`), allowing our Synology n8n instance to trigger generation over LAN via `http://192.168.1.123:50000/tts`.
The Windows VM hosts the CUDA 12.x PyTorch runtime. I exposed a lightweight FastAPI wrapper inside CosyVoice’s runtime script (`webui.py`), allowing my Synology n8n instance to trigger generation over LAN via `http://gpu-node.internal:50000/tts`.
---
## 4. Managing GPU Memory: Resident vs. On-Demand
Leaving every AI model loaded simultaneously will exhaust 12GB of VRAM instantly. We designed a dual-tier lifecycle:
Leaving every AI model loaded simultaneously will exhaust 12GB of VRAM instantly. I designed a dual-tier lifecycle:
1. **CosyVoice 2 as the Resident Daemon**: Consuming only **3.2 GB of VRAM**, CosyVoice 2 runs as an auto-starting background service when Windows boots (`shell:startup\CosyVoice 2.lnk`). This leaves 8.8 GB free for general desktop and lightweight inference.
2. **One-Click State Toggle Batches**: For heavy tasks (such as running 1080p video generation in ComfyUI or 14B LLMs), we created an idempotent batch script on the desktop:
2. **One-Click State Toggle Batches**: For heavy tasks (such as running 1080p video generation in ComfyUI or 14B LLMs), I created an idempotent batch script on the desktop:
```bat
@echo off
@@ -94,16 +94,16 @@ A single double-click halts the process and reclaims all 3.2GB within 500ms; cli
## 5. Integrating with Synology N8N
In n8n, we configured a dedicated workflow: **`Local TTS API (CosyVoice)`** listening on `/webhook/ltts`.
In n8n, I configured a dedicated workflow: **`Local TTS API (CosyVoice)`** listening on `/webhook/ltts`.
To ensure total interoperability with existing scripts, the workflow accepts both:
- **`GET /webhook/ltts?text=Hello&voice=child`**: Ideal for browser testing and simple URL triggers.
- **`POST /webhook/ltts`** with JSON body `{"text": "...", "voice": "..."}`: Used by our bots and backend services.
- **`POST /webhook/ltts`** with JSON body `{"text": "...", "voice": "..."}`: Used by bots and backend services.
The n8n pipeline consists of three streamlined nodes:
1. **Webhook Node**: Receives incoming requests.
2. **Parse & Clean Text (Code Node)**: Normalizes character encodings and defaults voice selections.
3. **HTTP Request Node**: Forwards the JSON payload to `http://192.168.1.123:50000/tts` and streams the binary `audio/wav` response directly back to the requester.
3. **HTTP Request Node**: Forwards the JSON payload to `http://gpu-node.internal:50000/tts` and streams the binary `audio/wav` response directly back to the requester.
---
@@ -111,7 +111,7 @@ The n8n pipeline consists of three streamlined nodes:
CosyVoice 2 synthesizes speech using zero-shot voice cloning: feeding a 3- to 5-second clean audio sample allows the model to clone the acoustic timbre instantly.
We packaged a library of curated reference samples into `asset/voices/` and mapped them to intuitive alias parameters:
I packaged a library of curated reference samples into `asset/voices/` and mapped them to intuitive alias parameters:
| Voice Key | Style Profile | Sample Source |
| :--- | :--- | :--- |
@@ -131,6 +131,16 @@ Switching voices is as simple as passing `&voice=child` in the query string or `
Running on the RTX 3060:
- **Generation Speed**: Real-time factor (RTF) averages **0.18**. A 10-second sentence renders in under **1.8 seconds**.
- **Latency**: First audio packet arrives at n8n in approximately **320ms**, enabling near-instant voice replies on Telegram.
- **Cost & Privacy**: Zero monthly cloud API charges, zero audio data leaving our local subnet, and complete immunity to external provider outages.
- **Cost & Privacy**: Zero monthly cloud API charges, zero audio data leaving my local subnet, and complete immunity to external provider outages.
Self-hosting modern neural TTS is no longer an exercise in brittle compile scripts and robotic phoneme tables. With open-source diffusion models and a modest 12GB workstation GPU, you can run a private, high-fidelity voice engine that surpasses commercial cloud offerings in customization and control.
---
## Want a private AI voice pipeline for your business?
If your support bots, content workflows, or internal tools are paying recurring cloud TTS bills — or struggling with bilingual Chinese-English code-switching — I design and deploy self-hosted speech and n8n automation stacks on hardware you control.
- 📱 **WhatsApp:** [+60 12-797 2969](https://wa.me/60127972969)
- 📧 **Email:** [[email protected]](mailto:[email protected]?subject=Self-hosted%20TTS%20pipeline)
- 🌐 **Website:** [hoelee.com](https://hoelee.com)
@@ -3,6 +3,8 @@ title: "How I Self-Hosted a Mem0 Memory Stack (So My AI Agent Remembers)"
description: "A private, self-hosted memory layer for AI agents — mem0 API + LiteLLM + PostgreSQL with pgvector on a Synology NAS. Semantic memory under your control, not a third party's."
pubDate: 2026-09-16
category: case-studies
blueprint: ai-agent-automation
blueprintLayer: L2-llm-gateway-agents
tags: [mem0, docker, postgres, pgvector, litellm, ai, sharing]
ogImage: /og/self-hosting-mem0-memory-stack.jpg
banner: /banners/self-hosting-mem0-memory-stack.jpg
@@ -43,7 +45,7 @@ the three things memory needs:
1. **Add a memory** — `POST /memories` with a fact or a conversation; it
extracts, dedupes, and stores it (optionally classified by an LLM).
2. **Search** — `GET /search` with a query returns the most relevant stored
2. **Search** — `POST /search` with a JSON payload returns the most relevant stored
facts, ranked by semantic similarity.
3. **User scoping** — memories belong to a `user_id`, so you can keep memory
per customer, per project, or per conversation without cross-talk.
@@ -77,6 +79,9 @@ Three containers on my Synology DS1821+, as a Portainer compose stack:
A representative compose looks like this:
<details>
<summary>📄 docker-compose.yml (30 lines) — Mem0 + LiteLLM + pgvector Production Stack</summary>
```yaml
services:
mem0-postgres:
@@ -102,12 +107,14 @@ services:
OPENAI_API_KEY: ${OPENAI_API_KEY}
OPENAI_API_BASE: http://mem0-litellm:4000/v1 # go through the gateway
MEM0_API_KEY: ${MEM0_API_KEY} # X-Api-Key for /search & /memories
POSTGRES_URL: postgresql://mem0:${MEM0_DB_PASSWORD}@mem0-postgres/mem0
POSTGRES_URL: postgresql://mem0:***@mem0-postgres/mem0
ports:
- "20015:8000"
restart: unless-stopped
```
</details>
(Mine is built from the mem0 repo with the model/fact-extraction directed
through LiteLLM; the exact image tag is me maintaining a local build, and I
keep the real API key out of the compose file — it's an env secret.)
@@ -117,9 +124,9 @@ keep the real API key out of the compose file — it's an env secret.)
Once it's up, the API is refreshingly small. Add a fact (fast, verbatim):
```bash
curl -X POST http://192.168.1.1:20015/memories \
curl -X POST http://nas-node.internal:20015/memories \
-H "Content-Type: application/json" \
-H "X-Api-Key: $MEM0_API_KEY" \
-H "X-Api-Key: ***" \
-d '{
"user_id": "customer-2211",
"text": "Customer 2211 prefers email over WhatsApp for order updates."
@@ -129,9 +136,9 @@ curl -X POST http://192.168.1.1:20015/memories \
Retrieve the most relevant memories later:
```bash
curl -X POST http://192.168.1.1:20015/memories/search \
curl -X POST http://nas-node.internal:20015/search \
-H "Content-Type: application/json" \
-H "X-Api-Key: $MEM0_API_KEY" \
-H "X-Api-Key: ***" \
-d '{
"user_id": "customer-2211",
"query": "how should I reach this customer?",
@@ -10,6 +10,7 @@ tags: ["ai", "agents", "ast", "performance", "architecture"]
ogImage: "/og/slashing-agent-context-with-a-code-graph.jpg"
banner: "/banners/slashing-agent-context-with-a-code-graph.jpg"
draft: false
dynamicCta: true
---
## The Problem
@@ -120,14 +121,16 @@ Move graph indexing into the local Git pre-commit hook and CI build step. The in
## The Result: Quantified In-Production Metrics
We benchmarked 20 identical bug-fix and feature-addition tasks across our service stack, comparing raw directory search against the AST code graph:
We benchmarked 20 identical bug-fix and feature-addition tasks across our service stack, comparing raw directory search against the AST code graph.
| Metric | Raw Text / Grep Search | AST Code Graph (`codegraph`) | Improvement |
*Benchmark Environment: Evaluated across Claude 3.5 Sonnet and GPT-4o (2024-11-20). All tasks represented production tickets involving cross-module dependencies. Token billing calculated using standard list prices ($3.00/1M input, $15.00/1M output).*
| Metric | Raw Text / Grep Search | AST Code Graph (`codegraph`) | Improvement / Speedup |
|---|---|---|---|
| **Average Initial Context Payload** | 41,200 tokens | **1,850 tokens** | **-95.5%** |
| **Cumulative Session Token Churn** | 184,000 tokens | **32,400 tokens** | **-82.4%** |
| **Turns to First Valid Patch** | 7.4 turns | **2.1 turns** | **3.5x faster** |
| **Signature / Type Regressions** | 18% of patches | **0%** | **Eliminated** |
| **Session Cost per Feature** | ~$1.28 | **~$0.19** | **85% cost reduction** |
| **Average Initial Context Payload** | 41,200 tokens | **1,850 tokens** | **-95.5% payload reduction** |
| **Cumulative Session Token Churn** | 184,000 tokens | **32,400 tokens** | **-82.4% token reduction** (5.68× information density) |
| **Turns to First Valid Patch** | 7.4 turns | **2.1 turns** | **-71.6% turn reduction** (3.52× faster patch delivery) |
| **Signature / Type Regressions** | 18% of patches | **0%** (zero missed types or parameter misorders) | **Eliminated** |
| **Session Cost per Feature** | ~$1.28 | **~$0.19** | **85.2% cost reduction** |
By treating code as a graph rather than raw text, the agent stops drowning in irrelevant lines and focuses 100% of its reasoning capacity on the precise logic that needs to change.
@@ -7,6 +7,7 @@ tags: [llm, api, deepseek, glm, alipay, malaysia]
ogImage: /og/using-chinese-llm-apis-from-malaysia.jpg
banner: /banners/using-chinese-llm-apis-from-malaysia.jpg
draft: false
dynamicCta: true
---
Chinese frontier models — GLM-5.3, DeepSeek V4, Qwen3.8-Max, Kimi K3 — now
@@ -15,12 +16,12 @@ frontier APIs. For a solo developer or a small business running AI automation,
that price difference is the difference between "AI that pays for itself" and
"AI that's too expensive to run."
But using them from Malaysia means crossing three walls first: paying in RMB,
But using them from Malaysia means crossing three walls first: payment rails,
passing mainland-China phone verification, and (on some platforms) real-name
(实名) registration. This is what I actually did, what broke, and what I'd do
differently.
## Why bother?
## Cost vs. Complexity: Why It's Worth Exploring
Chinese API pricing is often 3–10× cheaper than the equivalent Western model.
As of this writing, DeepSeek's V4.1 Flash is around **US$0.15–0.30 per 1M
@@ -28,21 +29,19 @@ input tokens and US$0.60–1.20 per 1M output tokens** (off-peak hours are half
price; cached input is ~20× cheaper still). A US$10 top-up goes a long way —
tens of millions of tokens for agent workloads if you route carefully.
The catch: most of these platforms are built for mainland developers. That
The catch: most of these platforms were originally built for mainland developers. That
means Chinese payment rails and, frequently, a mainland phone number for SMS
verification.
## Wall #1 — paying in RMB
## Wall #1 — Payment Rails and Compliance
I already had my own Alipay account, which is the payment method most of these
platforms accept (Alipay top-up on the platform itself, or Alipay to pay a
gateway). The problem is keeping RMB in it from Malaysia.
Payment settlement is the first barrier when accessing frontier models across borders. In practice, you must distinguish between **personal sandbox experiments** and **enterprise production deployments**:
I reload my Alipay RMB balance through an exchange contact I've used for many
top-ups. It's a common gray-market route in Malaysia — do your own due
diligence, and never send more than you're comfortable losing. Once the RMB is
in Alipay, paying for API credits is exactly like paying for anything else in
China.
- **Personal Sandbox Testing**: For individual developers validating prompts and workflows, international Alipay accounts allow linking foreign Visa/MasterCard credit cards for direct top-ups on supported gateways.
- **Enterprise Production Architecture**: Production client systems must never depend on unofficial peer transfers or gray-market top-ups. Commercial systems demanding corporate invoicing, tax compliance, and auditable SLAs should adopt formal global routes:
1. **DeepSeek Global Platform**: Accepts international credit cards in USD directly and generates corporate receipts without requiring mainland entities or phone numbers.
2. **BytePlus ModelArk**: ByteDance's Singapore entity operates full enterprise USD billing and hosts inference data in Singapore/Malaysia regional clusters, satisfying corporate data residency requirements.
3. **Self-Hosted Gateway Failover (LiteLLM)**: Deploying LiteLLM on private infrastructure orchestrates multi-vendor routing, providing seamless failover across regional endpoints and fallback providers if a primary route degrades.
## Wall #2 — phone verification
@@ -9,10 +9,11 @@ tags: [local-llm, gpu, llama-cpp, lm-studio, agents, self-hosting, cost]
ogImage: /og/why-i-still-bought-a-local-gpu.jpg
banner: /banners/why-i-still-bought-a-local-gpu.jpg
draft: false
dynamicCta: true
---
I have a house full of self-hosted infrastructure — Docker stacks, a NAS, a
tunnel, a small army of cron jobs. And I still pay for frontier models.
Across the self-hosted production infrastructure I manage — Docker stacks, NAS
storage, tunnels, and automated cron pipelines — I still pay for frontier models.
So when people find out I run local models too, the question is always the same:
*Claude and GPT are stronger. Local models are a pain to set up, slower, and
@@ -133,6 +134,10 @@ that you find out where they break.
The value of local compute is that you move that trial-and-error into a fixed
cost. That is a different question from "how much does a million tokens cost."
Here is the math on a real engineering workload: a typical agent loop with tool-use iterating through tests and refactoring takes 30–50 turns. Because each turn carries previous tool outputs and conversation history, an average turn easily loads 15k–25k tokens of context — consuming 600k to 1.2M tokens in a single agent run. If you are tuning a new autonomous workflow and test it 20 times a day, you burn 15M to 24M tokens. On frontier APIs like Claude 3.5 Sonnet ($3/$15 per million tokens), that single day of debugging costs $50–$150; on Opus, it crosses several hundred dollars.
On my own self-hosted GPU node (a dedicated 12 GB VRAM card running quantized Qwen 2.5 via LM Studio / llama.cpp), the entire rig pulls under 200W at full load. Running 20 iterations or 100 iterations adds cents in electricity and $0 in marginal API bills. The real math is not just "cost per million tokens" — it is whether you can afford to let an agent fail enough times to become reliable.
---
## Open models crossed the useful line, and the line keeps moving
@@ -10,6 +10,7 @@ tags: ["yue2", "comfyui", "ai-music", "local-ai", "gpu", "vram", "mandarin"]
ogImage: "/og/yue2-on-a-12gb-gpu-songs-that-lost-their-endings.jpg"
banner: "/banners/yue2-on-a-12gb-gpu-songs-that-lost-their-endings.jpg"
draft: false
dynamicCta: true
---
YuE2 is a 3B open-weights music model that sings Mandarin from a style prompt
and a lyric sheet. I ran it locally on a 12 GB RTX 3060 through ComfyUI —
@@ -63,7 +64,7 @@ curl -s -X POST http://127.0.0.1:20129/v1/audio/transcriptions \
-F "file=@t.wav" -F "language=zh"
```
The results were unambiguous:
(For tracks that had not even reached the final chorus, I widened the slice window back to the final 30 seconds with `-ss 210` to locate the active section when truncation hit.) The results were unambiguous:
| Song | Sung up to | What was missing |
|---|---|---|
@@ -147,7 +148,7 @@ Measured peak VRAM while rendering, sampled every five seconds with
| 281 s | 12.06 GB |
| 300 s | 12.03 GB |
The card has 12.288 GB. **300 seconds is the practical ceiling** — past that
The card has 12,288 MiB (12 GB) of VRAM. **300 seconds is the practical ceiling** — past that
you are one desktop browser tab away from an out-of-memory abort in the middle
of a six-minute render.
@@ -3,6 +3,8 @@ title: "我如何用 FLUX.1 Kontext 打造 AI 家具合成演示"
description: "用 fal.ai FLUX.1 Kontext 把家具产品照合成到风格化房间场景——多图端点、tainted canvas 的修复,以及为什么必须关掉 prompt enhancement。"
pubDate: 2026-09-11
category: case-studies
blueprint: sme-digital-suite
blueprintLayer: L1-high-conversion-web
tags: [fal-ai, flux, ai-image, javascript, canvas, ecommerce]
ogImage: /og/zh-ai-furniture-compositing-with-flux-kontext.jpg
banner: /banners/ai-furniture-compositing-with-flux-kontext.jpg
@@ -29,6 +31,9 @@ banner: /banners/ai-furniture-compositing-with-flux-kontext.jpg
做概念验证我不想搭后端。fal.ai 暴露了一个纯 REST 队列 API,所以一个 `index.html` 配 vanilla JavaScript 就能搞定全部。但这是三步,不是一步:
<details>
<summary>index.html — fal.ai REST queue submission & polling (32 lines)</summary>
```js
// 1. 提交——返回 request_id + 轮询 URL
const submit = await fetch(
@@ -64,6 +69,8 @@ const result = await (await fetch(response_url, {
// result.images[0].url 就是生成的图片
```
</details>
有两件事我很早就验证了,因为它们决定了「单文件」方案成不成立:
1. **CORS 是开放的。** `queue.fal.run` 返回宽松的 `access-control-allow-origin`,并允许 `authorization` 头,所以浏览器可以直接调用,无需代理。我在写任何 UI 之前就用 preflight 确认了这一点。
@@ -104,7 +111,7 @@ demo 在我脑子里能跑,在浏览器里却不行。我一点「生成」就
## Bug #2 —— API key 就在 HTML 里
对 demo 来说这可以容忍;对任何公开的东西来说这是个洞。客户端 `fetch` 里的 `Key` 头意味着 key 就在页面源码里,任何人按 F12 就能读到。给我交给客户的概念验证没问题,上生产就不可接受。
对 demo 来说这可以容忍;对任何公开的东西来说这是个洞。客户端 `fetch` 里的 `Key` 头意味着 key 就在页面源码里,任何人按 F12 就能读到。在交付客户内部预览期间,该静态页面仅搭配了极低余额的限额测试 Key 并在受控私密链接下访问;而在下一阶段的正式 WordPress 插件中,Key 将彻底移至服务端 PHP。
真正 WordPress 插件(下一阶段)的计划是把 key 挪到**服务端 PHP**:插件的端点去做带鉴权的 fal.ai 调用并返回图片,浏览器只跟插件自己的路由对话。demo 证明了管线能跑通;插件会把秘密放回秘密该待的地方。
@@ -126,7 +133,7 @@ demo 在我脑子里能跑,在浏览器里却不行。我一点「生成」就
1. **参考图不一致。** 最初的 `chair.jpg` 是一张完整的「桌+椅」*场景*照,不是干净的单椅——于是模型看到一张图就已经满足「桌+椅同框」,就倾向照着它来。
2. **按位置指代不可靠。** Kontext 按图片*内容*匹配,而不是数组顺序。说「第一张图」/「第二张图」并不能可靠地映射到「桌子」/「椅子」。修复是直接*命名物体*——*「the dining table」*和*「the chair」*——让模型自己去匹配正确的参考图。
3. **prompt enhancement 开着。** fal.ai 的 `enhance_prompt`(默认关,但我一直惦记着它)会把你的提示词改写成更丰富的审美描述——当目标是*「别重新解读产品」*时,这正好帮倒忙。我把它钉死为 `false`,让模型遵守字面提示词,而不是添油加醋。
3. **测试初期开启了 prompt enhancement。** fal.ai 的 `enhance_prompt` 参数一旦开启,会在后台把提示词改写成更丰富的审美描述——当目标是*「绝不要重新解读产品」*时,这正好帮了倒忙。我将其显式锁定为 `enhance_prompt: false`,强制模型严格执行字面约束,而不是添油加醋。
## 最终起作用的提示词
@@ -6,13 +6,14 @@ category: ai
tags: [ai-video, video-generation, ltx, open-weights, pricing, veo]
ogImage: /og/zh-best-ai-video-generators-2026.jpg
banner: /banners/best-ai-video-generators-2026.jpg
dynamicCta: true
---
2026 年,你究竟该用哪个 AI 视频生成器?其中又有多少是真免费的?
AI 视频是我用过的工具里变化最快的领域之一。一年前,像样的视频生成意味着付费订阅再加一点运气;今天,你既可以用开源权重模型在自己的 GPU 上生成视频和同步音频,也能从六七个平台领到真正可用的免费额度。
但"免费"这个词里藏着一个陷阱:在这个市场里它有至少四种含义,而大多数文章不会告诉你它说的是哪一种。这份指南对比了 2026 年真正能用的主要选择,把营销话术和现实分开,最后以一个亲手实验收尾——我用开源权重模型 **LTX-2.3**(通过 PinkCherry)生成了一段短视频,并会把结果展示给你看。
但"免费"这个词里藏着一个陷阱:在这个市场里它有至少四种含义,而大多数文章不会告诉你它说的是哪一种。这份指南对比了 2026 年真正能用的主要选择,把营销话术和现实分开,最后以一个亲手实验收尾——我用开源权重模型 **LTX-2.3** 在自托管的 ComfyUI 生产节点(内部工作流代号 **PinkCherry**)生成了一段短视频,并会把结果展示给你看。
*下文的价格与免费额度核实于 2026 年 9 月 11 日。这个市场每个月都在变——下单前请务必到服务商当前的价格页确认。*
@@ -23,7 +24,7 @@ AI 视频是我用过的工具里变化最快的领域之一。一年前,像
1. **每日或每月免费额度**——你获得一定数量的积分,定期刷新。Pika(每月 80 积分)、PixVerse(每日)和 Kling(每日 66)都属于这一类。
2. **一次性试用**——给你一笔额度,用完就再也没有了。Runway 的 125 积分是最典型的例子。
3. **开源权重**——模型本身可以免费下载和运行。但"模型免费"不等于"算力免费":你仍然需要 GPU、电费和搭建时间。
4. **托管演示或社区平台**——别人替你跑模型(比如我测试 LTX-2.3 用的 PinkCherry)。额度、队列和可用性都可能随时变化。
4. **托管演示或社区平台**——别人替你跑模型(比如 Hugging Face Spaces 或 fal.ai 的在线 Playground)。额度、队列和可用性都可能随时变化。
同一个词,成本结构天差地别。平台宣传"免费 AI 视频"时通常指第 1 或第 2 种;模型卡上写"免费"通常指第 3 种——成本悄悄转移到了你的硬件上。
@@ -146,7 +147,7 @@ Runway 已经变成一个视频*工作台*:自有 Gen-4.5 模型,加上真
## 我的 LTX-2.3 实测
这段短片是我用 **LTX-2.3** 在我自己的 ComfyUI 环境里生成的——一个我昵称为 **PinkCherry** 的本地安装。这是一个真实世界的测试,不是基准评测——我不对速度或质量优势做任何宣称,只是展示这个模型在我给出提示词、放手让它跑的时候做了什么。
这段短片是我用 **LTX-2.3** 在我自己的 ComfyUI 环境里生成的——一个部署在我的 RTX 3060 12GB 本地工作站、内部工作流代号为 **PinkCherry** 的生产节点。这是一个真实世界的测试,不是基准评测——我不对速度或质量优势做任何宣称,只是展示这个模型在我给出提示词、放手让它跑的时候做了什么。
<video controls playsinline preload="metadata" poster="https://content.hoelee.com/file/hoelee/video/LTX23-Demo.jpg">
<source src="https://content.hoelee.com/file/h_480/hoelee/video/LTX23-Demo.mp4" type="video/mp4" />
@@ -159,7 +160,7 @@ Runway 已经变成一个视频*工作台*:自有 Gen-4.5 模型,加上真
- **成本结构不同。** 没有订阅,没有按条计的 API 账单。成本转移到了 GPU 硬件、电费和搭建时间上——这是一次性投入(可以摊薄),不是持续支出。
- **掌控力更强。** 可以在 ComfyUI 里运行、更换检查点、量化以适配显存,还能在社区许可下微调。
- **它升级得很快。** 我测试的是 2.3;一个月后 2.5 就带着多镜头和 4K HDR 发布了。开源权重意味着你不必等某家公司给你升级。
- **它升级得很快。** 我测试的是 2.3;5 个月后(2026 年 8 月)2.5 就带着多镜头和 4K HDR 发布了。开源权重意味着你不必等某家公司给你升级。
诚实的提醒:结果很大程度上取决于硬件、工作流、模型版本和量化方式。托管游乐场(比如 Hugging Face Space 或 ComfyUI.cloud)属于"免费"的第 4 种含义——适合第一次尝鲜,但队列和额度会变;要认真本地使用,意味着要学习 ComfyUI。
@@ -173,7 +174,7 @@ Runway 已经变成一个视频*工作台*:自有 Gen-4.5 模型,加上真
- **视频和音频的 VAE 分开。** LTX-2.3 在同一次生成中产出同步音频,需要独立的音频 VAE 配合视频 VAE——正是这一点让声音成为生成过程的一部分,而不是事后贴上去的。
- **Gemma 3 12B 文本编码器。** LTX 系列用 Gemma 作为语言主干,配合 LTX 文本投影加载。
- **蒸馏检查点 + LoRA。** 快速蒸馏变体配蒸馏 LoRA(强度 0.6),**24 fps、5–7 秒片段**——这是 LTX 最实用的甜点区间。
- **GGUF 量化 + Chunk FeedForward。** 模型以 GGUF 量化形式加载,配合分块前馈层——22B 模型就是这么塞进消费级显存的。
- **GGUF 量化 + Chunk FeedForward。** 模型以 GGUF 量化形式加载,配合分块前馈层——19B/22B 参数级模型就是这么塞进我的 12GB 显存工作站的。
- **负向音频引导(NAG)。** 视频轨和音频轨各有一套负向提示词(比如音频用"画外音、旁白、镜头外说话"),保证生成的声音干净。
- **一个小型预览 VAE**,用于采样过程中的快速预览,不用每次都完整解码。
@@ -185,7 +186,7 @@ Runway 已经变成一个视频*工作台*:自有 Gen-4.5 模型,加上真
4. **一致性:** 避免与参考图矛盾的指令——它们会降低生成质量。
<details>
<summary>我工作流里的完整提示词(点击展开)</summary>
<summary>我工作流里的完整提示词(英文原版,195 词 / 点击展开)</summary>
> A cinematic futuristic night scene in a dense neon-lit city alley during a heavy rainstorm. A young Asian female courier in a dark waterproof jacket, black cargo pants and a compact glowing backpack runs quickly toward the camera, water splashing from her boots with every step, loose strands of wet hair moving naturally in the wind. The shot begins behind and slightly above her as she runs through the narrow alley, then the camera smoothly tracks alongside her and arcs around to the front, revealing her focused face as she looks briefly toward the camera while continuing to run. A small sleek hovering surveillance drone follows several meters behind her, its white searchlight sweeping through the rain. Neon signs reflect vividly across the wet pavement, puddles ripple from raindrops, mist drifts through the alley, and colored light flickers across her face and clothing. The camera movement remains smooth and cinematic with realistic handheld micro-motion, shallow depth of field, natural motion blur and strong foreground-to-background parallax. The final moment shows her rushing past the camera while the drone flies overhead, leaving the camera facing the glowing rainy alley. Realistic cinematic lighting, physically believable rain and water interaction, detailed skin, fabric and wet surfaces, high environmental detail, dramatic science-fiction atmosphere. Audio: heavy rainfall, footsteps splashing through puddles, distant city traffic, subtle electrical ambience and the quiet mechanical hum of the hovering drone.
@@ -7,6 +7,7 @@ tags: ["llm", "rag", "agents", "self-hosting", "whisper", "mem0"]
ogImage: /og/zh-five-step-ai-roadmap-what-i-actually-run.jpg
banner: /banners/five-step-ai-roadmap-what-i-actually-run.jpg
draft: false
dynamicCta: true
---
每隔几周就会有一张图在各个信息流里转一圈,到我这里的时候已经是翻译过的版本,发它的账号我也没关注。图里把「做大模型」这条路分成五步:先学基础 —— Transformer、神经网络、注意力机制,学到能看懂图就停;再学提示词工程,几天就能上手;然后学 RAG,让模型基于你自己的数据回答,而不是张口乱编;接着学 Agent,说是这个领域最大的风口;最后是微调,这里给的建议是预训练完全不用碰(那是大厂的事),把多模态微调练熟就行。每一步后面都挂着一个薪资数字,而正是最后那个数字让这张图到处流传。
@@ -17,7 +18,7 @@ draft: false
不信任那张图的理由,不在于条目本身错了,而在于路线图回答的是「该学什么」,而工作问的是「你能不能让东西跑起来」。这是两套不同的能力,而多数「五步学完 AI」的人就卡在这道缝里:他们能描述一条 RAG 流水线,但从来没有在晚上十一点被它搞崩过一次,也就没有在一晚上里真正搞懂检索、成本、数据接入和评估这四件事。
接下来我会把五个步骤逐一对应到我实际在跑的东西上,明说哪几步我从来没做过,最后给出如果重新来一次我会选的顺序。下面所有说法,都跑在一台装了 RTX 3060(12 GB)的台式机和一台 NAS 上。
接下来我会把五个步骤逐一对应到我实际在跑的东西上,明说哪几步我从来没做过,最后给出如果重新来一次我会选的顺序。下面所有说法,都跑在本地工作站(RTX 3060 12 GB)与私有 NAS 节点上。
## 第一步:基础 —— 够用就停
@@ -1,6 +1,6 @@
---
title: "从一首歌到 4 分钟完整 MV:我如何用 Gemini、AI 生成与 Filmora 剪辑串起全流程实操"
description: "基于《月牙湾》1080P 高清动漫 OP 制作实录:详解 AI 音乐重构、Gemini Argon 4 提示词分镜规划、文字生图与图生视频多次抽卡调优,以及 Filmora 剪辑与特效合成的真实生产实践。"
description: "基于《月牙湾》1080P 高清动漫 OP 制作实录:详解 AI 音乐重构、Gemini 提示词分镜规划、文字生图与图生视频多轮采样筛选调优,以及 Filmora 剪辑与特效合成的真实生产实践。"
pubDate: 2026-10-25
category: case-studies
blueprint: ai-agent-automation
@@ -49,11 +49,11 @@ draft: false
---
## 2. Gemini Argon 4 分镜设计:两阶段提示词规划
## 2. Gemini 分镜设计:两阶段提示词规划
很多人用大模型生成分镜时,习惯把整首歌歌词丢给它,说“帮我生成 24 个生图提示词”。这样做的结果往往惨不忍睹:前 3 个镜头写得很详细,到了第 10 镜以后提示词就只剩两三句话,甚至连主角长相都丢了。
在 Gemini(Argon 4)中,我采用**分阶段结构化输出**:
在 Gemini 中,我采用**分阶段结构化输出**:
1. **第一阶段:锁定视觉核心 DNA(Visual Constants)**
* **角色固定锚点**:固定主角发型(黑长直双环髻、红丝带发簪)、服装(青白双层汉元素织物)、外貌气质与年龄;
@@ -66,9 +66,9 @@ draft: false
---
## 3. 生图与图生视频的现实:多次“抽卡”与筛选调优
## 3. 生图与图生视频的现实:多轮采样与筛选调优
AI 视频制作中耗费最多时间的,就是**“生图 -> 挑图 -> 垫图生视频 -> 重抽挑视频”**的反复迭代过程。
AI 视频制作中耗费最多时间的,就是**“生图 -> 挑图 -> 垫图生视频 -> 重新采样挑选”**的反复迭代过程。
### ① 文字生图(T2I):解决画风与角色漂移
即使有详细的提示词,生图模型也常常会出现五官比例怪异、衣服颜色擅自更改、或在空镜头里莫名其妙画一张人脸的情况。
@@ -122,7 +122,7 @@ AI 生成出来的只是一个个孤立的视频切片,**把这些碎片变成
1. **AI 放大的是制作上限,但不能取代审美判断**:AI 可以在短时间内产出海量的画面与动作构想,但“哪一张图最好、哪一段动作能用、在哪个鼓点切镜头”,依然需要创作者的主观审美与导演直觉。
2. **多工具协同才是最优解**:
* **Gemini (Argon 4)** 负责文字创意、世界观定调与分镜拆解;
* **Gemini** 负责文字创意、世界观定调与分镜拆解;
* **AI 绘图与视频扩散** 负责提供高质感的视听资产;
* **FaceFusion (本地 RTX 3060)** 负责锁定女主角核心特写镜头与封面海报的面部一致性;
* **Filmora** 负责精准的时间轴控制、镜头拼接、声画对齐与视觉特效。
@@ -8,6 +8,7 @@ tags: ["windows", "mcp", "security", "ai-agents", "automation"]
ogImage: /og/zh-giving-ai-your-mouse-and-keyboard-safely.jpg
banner: /banners/giving-ai-your-mouse-and-keyboard-safely.jpg
draft: false
dynamicCta: true
---
让 AI 模型直接操控操作系统的物理鼠标和键盘,在演示视频里看起来非常惊艳:Agent 看着你的屏幕、点击按钮、输入文本,自动搞定那些没有提供 REST API 也没有 CLI 命令行接口的老旧桌面软件。
@@ -45,8 +46,8 @@ uvx windows-mcp serve --exclude-tools "PowerShell,Registry"
现代开发机环境普遍配备 2K(2560×1440)、4K 或多显示器拼接虚拟桌面。
当 Agent 为了定位一个元素而截取全屏快照时,原始画布尺寸的文件经常超过 2.5 MB。这会直接导致两类致命问题:
1. **MCP 载荷上限截断**:多数 MCP 客户端(如 Claude Desktop 或终端 Agent)对单次工具返回结果有 1 MB 的尺寸限制,全量 4K 截图会直接触发 `PayloadTooLarge` 报错中断。
在我平时使用的主力工作站上(1080p 副屏搭配 1440p 主屏,上下拼接构成了总计 2560×2520 的虚拟画布),未缩放的单张 PNG 截图体积高达 **2.8 MB**。这会直接导致两类致命问题:
1. **MCP 载荷上限截断**:多数 MCP 客户端(如 Claude Desktop 或终端 Agent)对单次工具返回结果有 1 MB 的尺寸限制,全量原始截图会直接触发 `PayloadTooLarge` 报错中断。
2. **上下文 Token 快速耗尽**:超大分辨率图片会吃光上下文窗口的 Token 预算,造成推理成本飙升并把先前的会话记忆过早挤出缓存。
### 修复方案
@@ -56,7 +57,7 @@ uvx windows-mcp serve --exclude-tools "PowerShell,Registry"
export WINDOWS_MCP_SCREENSHOT_SCALE="0.5"
```
0.5 的缩放将横纵像素各减半,在不影响元素识别的前提下将体积削减 75%。
0.5 的缩放将横纵像素各减半,在不影响元素识别的前提下将体积削减 75%(从 2.8 MB 暴降至约 620 KB)。
*坐标换算说明:* 启用缩放后,工具元数据会提示 `Screenshot Coordinate Scale: 2.0`。模型在读取缩放图像的像素坐标后,传给 `Click` 或 `Move` 时需要乘以 `2.0` 换算回实际物理屏幕像素。
@@ -83,7 +83,7 @@ AI 歌唱音频中普遍存在拖音、转音、颤音和节奏弹性拉伸。
2. 调用本地 `faster-whisper` 并开启词级时间戳(`word_timestamps=True`);
3. 针对每行歌词,锁定首字真实发音起振的能量起跑点,校准字幕起止点。
这样产出的 `.lrc` 与 `.srt` 具备声学校准精度,无论歌手怎么拖长音,高亮提示总是在唱出第一个辅音的瞬间精准点亮。
这样产出的 `.lrc` 与 `.srt` 即可精准对齐到**物理渲染帧**:在 30 fps 帧率下,单帧时长为 33.3 ms,这才是视频渲染真正起决定性作用的时间分辨率。当歌手唱出乐句第一个辅音的瞬间,高亮状态即在对应渲染帧切换。词级时间戳并非“微秒级精确”,这套管线也绝不作此夸大宣称——它将每行歌词对齐到声学起振点,并交由 33.3 ms 的物理帧网格做最终裁决。
---
+8 -5
View File
@@ -5,6 +5,7 @@ pubDate: 2026-09-04
category: case-studies
blueprint: sme-digital-suite
blueprintLayer: L1-high-conversion-web
dynamicCta: true
tags: [astro, gitea, ci-cd, self-hosting, docker, cloudflare]
ogImage: /og/zh-how-i-host-this-blog.jpg
banner: /banners/how-i-host-this-blog.jpg
@@ -47,6 +48,9 @@ DevOps 工作的一个活生生的演示。
公网暴露由 Cloudflare 通过隧道单独处理,所以这里的任何东西都不需要公网 IP
或开放端口。
<details>
<summary>docker-compose.yml — Gitea + act_runner + nginx (40 lines)</summary>
```yaml
# docker-compose.yml — Gitea + act_runner + nginx(博客站点根目录)
services:
@@ -87,6 +91,8 @@ services:
restart: unless-stopped
```
</details>
CI 任务(`deploy.yml`)先在 runner 上构建出 `dist/`,然后 `cp -r dist/*` 到
与 nginx 容器共享的 `./html` 卷里。Cloudflare 的隧道守护进程(`cloudflared`)
指向 `nginx-blog`——那是唯一暴露到公网的东西。
@@ -95,9 +101,7 @@ CI 任务(`deploy.yml`)先在 runner 上构建出 `dist/`,然后 `cp -r di
1. **`act_runner` 需要挂载 Docker socket**(`/var/run/docker.sock`)才能拉起
构建任务——没有它,任务会一直卡在排队状态。
2. **runner 的 `GITEA_RUNNER_REGISTRATION_TOKEN`** 是 Gitea 的一个 *Actions
secret*(workflow 里写成 `${{ secrets.PAT }}`),绝不会硬编码在仓库里——
所以这个仓库可以放心地公开。
2. **敏感凭据彻底外置**:宿主机注册 Runner 所用的 `GITEA_RUNNER_REGISTRATION_TOKEN` 通过宿主机未纳入版本管理的本地 `.env` 注入(`${RUNNER_TOKEN}`),而 CI 构建拉取代码所用的令牌则存储在 Gitea 的 Actions Secret 中(workflow 内引用为 `${{ secrets.PAT }}`),仓库源码中绝不硬编码任何凭据——因此整个仓库可以安全地完全公开。
## 安装时的坑(真实经历,来自我的 commit 历史)
@@ -121,8 +125,7 @@ CI 任务(`deploy.yml`)先在 runner 上构建出 `dist/`,然后 `cp -r di
3. **Gitea 与 `act_runner` 的版本不匹配。** 我花了三个 commit
("test gitea 1.24.7 + act_runner 0.2.13" → "gitea 1.25.5" → "gitea 1.27.3 +
runner 3.3.2")才让 runner 成功注册并领取任务。教训是:**让 runner 与 Gitea
的主版本号匹配**——runner 比 Gitea 落后一个主版本,就会静默注册失败。
runner 3.3.2")才让 runner 成功注册并领取任务。教训是:**关注 Gitea Actions 生态的架构迁移**——Gitea 1.27+ 将原有的 `act_runner` 重构演进为新版 `gitea/runner`(版本号跳转至 3.x 分支,配置项如 `container.valid_volumes` 等结构均有调整)。如果 runner 镜像或协议落后于 Gitea 实例,任务就会静默失败或无法拉起容器。
4. **runner 需要 `config_file` 环境变量。** 只有显式传入配置文件路径,runner
才会连到正确的实例。
@@ -161,7 +161,7 @@ v6 家族新增的控制功能,直接改变了我做事的方式:
<figure class="suno-track">
<audio controls preload="metadata" oncontextmenu="return false;" controlslist="nodownload" data-lrc="lrc-xinliang" src="https://content.hoelee.com/file/hoelee/music/%E6%96%B0%E9%9D%93.mp3"></audio>
<pre id="lrc-xinliang" hidden>[00:00.00]拥凝练韵文引导心灵净化,
<pre id="lrc-xinliang" hidden>[00:00.00]用凝练韵文引导心灵净化,
[00:06.43]在跃动节拍与禅意氛围间形成张力,
[00:12.87]达成冥想深度与律动活力的独特平衡随至…
[00:19.31]如梦如幻… 心境无常…
@@ -315,6 +315,9 @@ beats, reverse reverb brings dreamlike magic
**第三步——带元标签的歌词稿:**
<details>
<summary>泡泡星球漫游记 — Metatagged lyric sheet (38 lines)</summary>
```text
[Intro][Whistling][Indie][Girl]
彩云兜着阳光转圈圈
@@ -353,6 +356,8 @@ beats, reverse reverb brings dreamlike magic
[Fade to End]
```
</details>
注意元标签比裸歌词稿多约束了什么:段落*标签*(`[Bridge]`、`[Outro]`)、
*演唱方式*(`[Whistling]`、`[Giggles]`)、歌曲中间换*制作风格*
(`[Reverse Reverb]` 只在副歌)、桥段转风格(`[Fantasy Pop]`)。
@@ -77,13 +77,18 @@ WHOIS 记录或哪行 AUP 驱动的。
### 编排实际长什么样
大致上,每一批:
在智能体编排层,我派发了 3 个并行子任务,每个子代理隔离抓取并返回严格的 JSON 证据契约:
```text
子代理 → 「这是清单,这是你那 5 家服务商」
→ 逐家:抓 WHOIS、AUP、隐私政策、价格、评测
→ 返回 { 域名年龄, AUP红旗[], 留存条款, 口碑, 流量 }
我 → 合并进一张评分卡,套用清单,排序,写结论
```json
{
"provider": "example-vps",
"domain_registered_at": "2024-04-12",
"claimed_since": "2012",
"aup_prohibited_clauses": ["tunnel", "reverse_proxy", "tor"],
"log_retention_policy": "30_days_connection_metadata",
"metered_cap_tb": 1.0,
"flag_summary": "首页声称 2012 年成立,但域名注册仅 4 个月;AUP 条款明文禁止代理隧道。"
}
```
三个子代理并行跑。整轮——二十家服务商、每家五份文档、大约一百次抓取——在手工
@@ -94,7 +99,7 @@ WHOIS 记录或哪行 AUP 驱动的。
评分卡暴露了首页永远不会告诉你的真问题:
- **一家声称「自 2012 年起值得信赖」的服务商,域名比这晚得多。** WHOIS 显示域名是
同一年的某个月才注册的——一个「从业多年」的四位数声明,配一个才几个月的域名。
当年四月才注册的——首页挂着「Since 2012」的老牌资历声明,底层却是一个注册仅四个月的新域名。
这要么是换了马甲的壳,要么就是撒谎,无论哪种,它页面上所有其他声明在我眼里都要
降级。
@@ -8,6 +8,7 @@ tags: [ai-agent, shopee, automation, human-in-the-loop, ecommerce, cron]
ogImage: /og/zh-i-let-an-agent-manage-my-shopee-vouchers.jpg
banner: /banners/i-let-an-agent-manage-my-shopee-vouchers.jpg
draft: false
dynamicCta: true
---
我在 Shopee 上开了一家小店,卖数字商品。店还年轻,挡在第一笔订单前面的不是库存,而是一个**必须先在 30 天内完成一笔订单才能申请**的商家计划。所以:先有订单。
@@ -16,7 +17,7 @@ draft: false
结果并不顺手。不是自动化难,而是平台有一个特性:
**Shopee 的优惠券没有草稿态。** 商品可以点 *Save and Delist* 存成草稿,以后再决定;优惠券不行 —— `Confirm` 既是发布按钮也是花钱按钮,点下去买家就能领,每领一张就从你的 escrow 余额里扣。中间没有「已保存、未上线」这个可以躲的位置。
**Shopee 的优惠券没有草稿态。** 商品可以点 *Save and Delist* 存成草稿,以后再决定;优惠券不行 —— `Confirm` 既是发布按钮也是花钱按钮,点下去买家就能立刻领用下单,每一笔核销都会直接从你的订单结算款(escrow)里划走真金白银。中间没有「已保存、未上线」这个可以躲的位置。
这一句话决定了整套设计,也是我如果重做一次会最先做对的地方。
@@ -113,7 +114,7 @@ else:
## 结果
现在店里有四张券在跑,合起来正好构成一个按客单价分档的阶梯 —— 这才是原本的商业目标:
现在店里有四张券在跑,合起来正好构成一个按客单价分档的阶梯 —— 这才是原本的商业目标(注:上文的 JSON 片段为早期试跑模板,下方表格记录的则是最终线上稳定运行的四档规则):
| 篮子金额 | 折扣 | 买家实付 |
|---|---|---|
@@ -122,7 +123,7 @@ else:
| RM29 | RM14 off | **RM15** |
| 新买家 | RM4.50 off | **RM4.50** |
如果每一张的每一份都被领完,最坏敞口是 RM474。真实支出只是其中一小部分,因为只有订单完成才扣钱 —— 唯一的例外是那张我误建出来的 RM140 券,我留着它了,因为它正好就是我想补的 RM29 那一档。
如果每一张的每一份都被领完,最坏敞口是 RM474。真实支出只是其中一小部分,因为只有订单完成核销时才会实际扣款。而那张误建出来的 RM140 敞口券(RM29 减 RM14 × 10 张),我最终决定保留,因为它刚好补上了我原本就想建的 RM29 满减档位。
自打 propose 闸门装上之后,助手自己创建的券数量是 **0**。它现在做的事,是每天打印一段参数,然后等我点头。
@@ -127,8 +127,8 @@ Osmani 讲的 comprehension debt 就落在这里。循环跑得越顺,你越
如果你在跑任何必须一直活着的东西,那么「loop engineering」有用的那个版本不是一个 agent 舰队,而是:一个停止条件能被测试的 cron 任务、一个说得出「我看不见」的验证器、一条为没预料到的失败准备的出口,以及「允许它只做最小那件事」的权限。从你每周手动检查的那一件事开始,让它变成承重结构。
## 我目前的求职状态
## 想为你的业务部署这套自愈巡检体系?
我是全栈开发 / DevOps 工程师,正在找 remote 或 hybrid 的岗位——平台、基础设施、DevOps 或全栈方向。这个博客就是我的作品集:这里的每一篇,都是我真正做过、并且必须让它持续跑下去的东西,上面这 11 个循环是其中最不花哨、也最实用的部分。
如果你也在维护关键业务服务器、电商后台或私有化服务,不想等到客户投诉才发现服务宕机或证书过期,我可以为你定制并交付这套零 Token 消耗的确定性巡检与自愈架构。同时,我也开放远程或混合模式的 Senior Full-Stack / DevOps / 平台工程岗位机会。
**Email:[[email protected]](mailto:[email protected]) · [LinkedIn](https://www.linkedin.com/in/hoelee) · [GitHub](https://github.com/hoelee) · [hoelee.com](https://hoelee.com)**
**WhatsApp:[+60 12-797 2969](https://wa.me/60127972969) · Email:[[email protected]](mailto:[email protected]) · [LinkedIn](https://www.linkedin.com/in/hoelee) · [GitHub](https://github.com/hoelee) · [hoelee.com](https://hoelee.com)**
@@ -1,6 +1,6 @@
---
title: "用一个 Prometheus 监控 unRaid、群晖 NAS 和 VPS:我做错的四件事"
description: "三台主机、一个 Prometheus、一个 Grafana:覆盖矩阵、把同一个 NAS 卷算成三次的\"总容量\",以及那台完全不导出 CPU 频率的虚拟机。"
description: "三台主机、一个 Prometheus、一个 Grafana:覆盖矩阵、把同一个 NAS 卷算成三次的「总容量」,以及那台完全不导出 CPU 频率的虚拟机。"
pubDate: 2026-07-29
updatedDate: 2026-09-29
category: case-studies
@@ -14,7 +14,7 @@ draft: false
## 为什么这件事重要
当一套自托管基础设施在跑别人的网站、邮件和文件时,它就不是玩具了。糟糕的一周和糟糕的**一个月**之间,差别通常只是你多早发现:一块出现重分配扇区的盘、一个只剩 20 GB 的卷、一台被宿主机饿死的虚拟机、一个夜里重启四次的容器。
当一套自托管生产基础设施承载着客户的生产网站、企业邮件和业务文件时,可用性就是硬性契约。糟糕的一周和糟糕的**一个月**之间,差别通常只是你多早发现:一块出现重分配扇区的盘、一个只剩 20 GB 的卷、一台被宿主机饿死的虚拟机、一个夜里重启四次的容器。
我有三台机器——unRaid(日常主力)、群晖 NAS(存储与服务)和 VPS(对外)——以及三套**各自**都不知道它们在干什么的方式。这篇文章讲它们如何变成一块屏,以及我路上做错的四件事——其中三个你也会踩。
@@ -26,13 +26,13 @@ draft: false
|---|---|---|---|
| unRaid | node_exporter (`:9100`) | cAdvisor (`:8080`) | 直连 |
| 群晖 DSM | node_exporter (`:9100`) | cAdvisor (`:8082`) | 局域网 |
| ServerHosh VPS | node_exporter (`:9100`) | cAdvisor (`:8081`) | VPN 隧道 + `nginx` stream 中转 |
| Ubuntu 公网边缘 VPS | node_exporter (`:9100`) | cAdvisor (`:8081`) | VPN 隧道 + `nginx` stream 中转 |
**node_exporter 与 cAdvisor 不是替代关系——这是最容易搞错的一点。** node_exporter 看的是**主机**:CPU、内存、网络、磁盘、文件系统、温度;它对 cgroup 一无所知,说不出是哪个容器吃掉了内存。cAdvisor 只看**容器**。想要主机健康和按容器记账,两个都得跑;少一个,那个洞会在几个月后表现为一次无法解释的负载高峰。
## 错误一:cAdvisor 里那些\"不是容器的容器\"
## 错误一:cAdvisor 里那些「不是容器的容器」
我的容器规则开始对不是容器的东西告警。cAdvisor 会为 **systemd slice** 和整机 cgroup 导出序列——其中一条没有名字的序列报告了约 58 GB 的\"内存使用\",却没有对应的容器。那就是整台主机,被描述成了一个容器。
我的容器规则开始对不是容器的东西告警。cAdvisor 会为 **systemd slice** 和整机 cgroup 导出序列——其中一条没有名字的序列报告了约 58 GB 的「内存使用」,却没有对应的容器。那就是整台主机,被描述成了一个容器。
修法是加一个过滤条件,但你得先知道要写它:
@@ -41,9 +41,9 @@ draft: false
container_memory_working_set_bytes{job=~"cadvisor.*", name!=""}
```
少了 `name!=""`,一条\"容器内存超过 10 GB\"的规则会对主机自己告警。
少了 `name!=""`,一条「容器内存超过 10 GB」的规则会对主机自己告警。
## 错误二:我的\"总容量\"是虚构的
## 错误二:我的「总容量」是虚构的
聚合面板好看,但就是错的。原因:**同一个文件系统会在指标里出现多次。**
@@ -65,11 +65,11 @@ node_filesystem_size_bytes{
- **校验盘对内核不可见。** unRaid 的奇偶校验盘没有文件系统,所以从不出现在指标里——这个和是可用容量,不是硬盘数量。
- **镜像会虚增裸和。** 两块镜像 SSD 会各报一次自己的字节,和不是你能存的量。
还有那个以后一定会咬我的遗漏:unRaid 的 shfs 联合挂载(`/mnt/user`)报告 `avail=0`。把它算进\"剩余空间低于 X\"的规则里,就会产生一条永远无法解除的告警。
还有那个以后一定会咬我的遗漏:unRaid 的 shfs 联合挂载(`/mnt/user`)报告 `avail=0`。把它算进「剩余空间低于 X」的规则里,就会产生一条永远无法解除的告警。
## 错误三:那台不导出 CPU 频率的虚拟机
为了算全家的\"总 CPU 频率\",我直接用 node_exporter 的 cpufreq collector。unRaid 和 NAS 都按核心报出了 `node_cpu_scaling_frequency_hertz`;VPS **什么都没有**——它是 KVM 客户机,而客户机没有 `/sys/devices/system/cpu/cpu0/cpufreq`。这个指标在那里无法存在。
为了汇总跨节点的「总 CPU 频率」,我直接用 node_exporter 的 cpufreq collector。unRaid 和 NAS 都按核心报出了 `node_cpu_scaling_frequency_hertz`;VPS **什么都没有**——它是 KVM 客户机,而客户机没有 `/sys/devices/system/cpu/cpu0/cpufreq`。这个指标在那里无法存在。
修法是 textfile collector:一个小脚本读客户机**能**看到的东西(`/proc/cpuinfo`),把 Prometheus 格式指标写进 node_exporter 会抓取的目录:
@@ -103,20 +103,20 @@ sum(node_cpu_scaling_frequency_hertz or node_cpu_mhz_current_hz)
```yaml
services:
node_exporter:
hostname: 2.hoelee.com # otherwise `nodename` = the container ID
hostname: edge-vps.hoelee.internal # otherwise `nodename` = the container ID
```
## 我下次会怎么做
- **刻意把总览屏留到最后做。** \"总容量是多少\"这个问题会逼出全部去重工作;先做完三台主机面板再发现,就得在每个地方重做这个数字。
- **刻意把总览屏留到最后做。** 「总容量是多少」这个问题会逼出全部去重工作;先做完三台主机面板再发现,就得在每个地方重做这个数字。
- **一开始就给容器钉 hostname。** 一行成本,省掉一个让人困惑的下拉框。
- **假设每个虚拟化或家电式主机都会藏掉一类指标。** NAS 可能限制你的容器监控(DSM 的 Docker API 版本把我的 cAdvisor 钉在 v0.53.0——更新的版本要更新的 Docker API);虚拟机藏掉 cpufreq;路由器藏掉自己的 CPU。找缺口的方法是**数你期待的东西**,而不是相信\"采集器成功了\"。
- **假设每个虚拟化或家电式主机都会藏掉一类指标。** NAS 可能限制你的容器监控(DSM 的 Docker API 版本把我的 cAdvisor 钉在 v0.53.0——更新的版本要更新的 Docker API);虚拟机藏掉 cpufreq;路由器藏掉自己的 CPU。找缺口的方法是**数你期待的东西**,而不是相信「采集器成功了」。
## 结果
三台主机、**7 个抓取目标、一个 Grafana、16 条告警规则**,以及一块屏:**47 个 CPU 核心 · 当前 158 GHz(标称上限 205 GHz)· 142 GB 内存 · 64 TB 存储 · 155 个运行中容器**,并且每台机器克隆同一套主机/容器面板布局,三台读起来完全一致。告警进 Telegram 和邮件,Prometheus 保留 30 天。
三台主机、**7 个抓取目标、一个 Grafana、16 条告警规则**,以及一块屏:**47 个 CPU 核心 · 当前 158 GHz(标称上限 205 GHz)· 142 GB 内存 · 64 TB 存储 · 155 个运行中容器(注:此为 2026-07 初始监控基线,后续随业务扩容至 162 个)**,并且每台机器克隆同一套主机/容器面板布局,三台读起来完全一致。告警进 Telegram 和邮件,Prometheus 保留 30 天。
最有用的产出不是仪表盘,而是重写存储规则那天触发的一条告警:一个只剩 21 GB 的 NAS 卷——它一直藏在\"99% 满\"的百分比阈值背后,而那个巨物早已变成背景噪声。
最有用的产出不是仪表盘,而是重写存储规则那天触发的一条告警:一个只剩 21 GB 的 NAS 卷——它一直藏在「99% 满」的百分比阈值背后,而那个巨物早已变成背景噪声。
## 需要为你的业务做这个吗?
@@ -8,6 +8,7 @@ tags: ["windows", "mcp", "ai-agents", "hermes", "opencode", "claw-code"]
ogImage: /og/zh-plugging-windows-desktop-control-into-four-ai-agents.jpg
banner: /banners/plugging-windows-desktop-control-into-four-ai-agents.jpg
draft: false
dynamicCta: true
---
在多 Agent 协同的日常开发体系中,针对不同场景调度专用 Agent 已成为常态。在我管理的这台 Windows 工作站上,常驻着 4 款不同的 AI 代码助手:
@@ -148,7 +149,7 @@ Claw Code(v0.1.7)支持在项目目录的 `.claw/settings.json` 或用户主
```bash
$ claw mcp
MCP
Working directory C:\Users\hoelee
Working directory C:\Users\workstation
Configured servers 1
Total entries 1
Invalid entries 0
@@ -160,19 +161,14 @@ MCP
## 4. Claude Code 接入
Claude Code 的全局配置文件为 `~/.claude.json`,结构完全兼容:
Claude Code 的全局配置文件为 `~/.claude.json`,结构与 Claw 完全兼容。直接将上述 `windows-mcp` 块镜像写入 `~/.claude.json` 即可:
```json
{
"mcpServers": {
"windows-mcp": {
"command": "uvx",
"args": [
"windows-mcp",
"serve",
"--exclude-tools",
"PowerShell,Registry"
],
"args": ["windows-mcp", "serve", "--exclude-tools", "PowerShell,Registry"],
"env": {
"WINDOWS_MCP_CONTROL": "false",
"WINDOWS_MCP_SCREENSHOT_SCALE": "0.5",
@@ -9,6 +9,7 @@ stream: creative-lab
tags: [ai-video, wan, comfyui, seedvr2, local-llm, gpu, cost]
ogImage: /og/zh-running-ai-video-locally-on-a-12gb-gpu.jpg
banner: /banners/running-ai-video-locally-on-a-12gb-gpu.jpg
dynamicCta: true
---
所有讲本地 AI 视频的文章对 12GB 显卡的说法都差不多:小模型能跑,勉强。没有一篇说清楚"勉强"折算成多少分钟,也没人说产出到底是不是真的 1080p,更没人说最先崩的是哪一环。
@@ -42,7 +43,7 @@ banner: /banners/running-ai-video-locally-on-a-12gb-gpu.jpg
| 分辨率 | 总计 | 采样 | 解码 | 显存峰值 |
|---|---|---|---|---|
| 832×480 | 6.1 分钟 | 4.8 分钟 | 1.3 分钟 | 10.3 GB |
| 1280×704 | 17.9 分钟 | 15.1 分钟 | 2.9 分钟 | 12.1 GB |
| 1280×704 | 17.9 分钟 | 15.1 分钟 | 2.8 分钟 | 12.1 GB |
像素数减半,时间不是减半,而是降到约三分之一。这是扩散 Transformer 典型的超线性关系,也正是 480p 这档草稿存在的理由。
@@ -351,7 +352,7 @@ curl -X POST http://127.0.0.1:8188/free \
# 每次跑之前先清显存
curl -X POST http://127.0.0.1:8188/free -d '{"unload_models":true,"free_memory":true}'
# 1. 按每秒 25 帧在 1280x736 生成
# 1. 生成 1 秒镜头(24fps 下 4k+1 规则对应 25 帧)在 1280x736
python bench_wan.py --w 1280 --h 736 --frames 25 --steps 20 --tag final_1s --cold
# 2. 每段不超过 41 帧,分别放大到 1920x1104
@@ -368,7 +369,7 @@ ffmpeg -i upscaled.mp4 -vf "crop=1920:1080:0:12" -c:v libx264 -crf 16 final.mp4
你的浏览器不支持 HTML5 视频。
</video>
*Wan 2.2 TI2V-5B,1280×704 → SeedVR2 (41 帧分段超分) → 1920×1080。7 镜头连续生成缝合并搭配 YuE2 本地生成的国风燃曲,没有一帧、一秒是别人渲染的。*
*Wan 2.2 TI2V-5B, 1280×736 → SeedVR2 1.5× (1920×1104, 41 帧分段超分) → 裁切 1920×1080。7 镜头连续生成缝合并搭配 YuE2 本地生成的国风燃曲,没有一帧、一秒是别人渲染的。*
## 放大到底买到了什么
@@ -384,7 +385,7 @@ ffmpeg -i upscaled.mp4 -vf "crop=1920:1080:0:12" -c:v libx264 -crf 16 final.mp4
| 2 秒(49 帧) | 5.7 分钟 | 7.7 分钟 | 约 13 分钟 | \$0 |
| 5 秒(121 帧) | 17.9 分钟 | 14.1 分钟 | 约 32 分钟 | \$0 |
8 个镜头、60 秒的片子,本地大约半天算力;走商业 API 大约 \$5,40 分钟。两个数字都是真的,回答的是不同问题。本地机器负责那三十条没人付钱的试错,API 负责最后那一条要交付的。
12 个 5 秒镜头组成的 60 秒成片,本地大约需要 6.4 小时算力(约半天连续运转);走商业 API 大约 \$5,40 分钟。两个数字都是真的,回答的是不同问题。本地机器负责那三十条没人付钱的试错,API 负责最后那一条要交付的。
## 如果重来一次
@@ -27,7 +27,7 @@ draft: false
| unRaid | CI runner、媒体流水线、监控 | 37 | 21 |
| Ubuntu 24.04 VPS | 公网入口、邮件平台、客户站点 | 39 | 13 |
这个划分是刻意的,不是历史遗留。VPS 承担的是「家里网断了也必须可达、必须快」的东西,因为公网流量和客户邮件都落在那里。Synology 承担需要磁盘和稳定文件系统的部分,以及所有其他服务都信任的身份源。unRaid 跑 CI runner 和那些又重又突发的批处理任务——否则它们会和服务抢内存。
这个划分是刻意的,不是历史遗留。公网 VPS 承担的是「即便本地私有化节点上行链路维护或波动,也必须保持低延迟与高可用」的核心入口服务,因为公网流量和客户邮件都落在那里。Synology 承担需要磁盘和稳定文件系统的部分,以及所有其他服务都信任的身份源。unRaid 跑 CI runner 和那些又重又突发的批处理任务——否则它们会和服务抢内存。
## 让三台主机像一套系统的东西
@@ -8,6 +8,7 @@ blueprintLayer: L3-local-gpu-inference
tags: [n8n, tts, azure, google-cloud, docker, automation, sidecar]
ogImage: /og/zh-running-tts-as-a-service-with-token-sidecars.jpg
banner: /banners/running-tts-as-a-service-with-token-sidecars.jpg
dynamicCta: true
---
我自建了一个电子书阅读服务器。它有个「朗读」功能,内置的引擎能用但很机械。所以我把它接到了真正的神经网络语音上——Azure Speech 和 Google Cloud TTS——通过我的 n8n 实例。
@@ -66,16 +67,19 @@ cron-azure-refresh:
image: curlimages/curl:8.10.1
restart: unless-stopped
volumes:
- /volume1/docker/n8n/file:/file
- /opt/tts-service/file:/file
entrypoint: /bin/sh
command: >
-c 'while true; do
curl -s -X POST "https://southeastasia.api.cognitive.microsoft.com/sts/v1.0/issueToken"
-H "Ocp-Apim-Subscription-Key: $AZURE_SPEECH_KEY" > /file/MicrosoftTTS/accesstoken.txt;
curl -s -f -X POST "https://southeastasia.api.cognitive.microsoft.com/sts/v1.0/issueToken" \
-H "Ocp-Apim-Subscription-Key: $AZURE_SPEECH_KEY" > /file/MicrosoftTTS/accesstoken.tmp && \
mv -f /file/MicrosoftTTS/accesstoken.tmp /file/MicrosoftTTS/accesstoken.txt;
sleep 570;
done'
```
*原子写入细节:* 普通的 shell 重定向 `> accesstoken.txt` 在命令发起的瞬间就会把文件截断为 0 字节。在网络请求耗时 200~500ms 的窗口期内,如果 n8n 恰好读取该文件,就会拿到空 token 导致 401 报错。先输出到临时文件 `.tmp` 再通过 `mv -f` 原子覆盖是保障并发读取安全的关键工程细节。
## 为什么用文件,而不是那些显而易见的替代方案
**为什么不把 token 存进 n8n 凭据、在工作流里刷新?** 因为刷新逻辑就会被复制进每一个需要 token 的工作流,而每一份拷贝都需要自己的错误处理。当凌晨三点刷新失败时,你希望只有一个进程需要关心这件事。
@@ -12,7 +12,7 @@ banner: /banners/self-healing-digital-goods-entitlements.jpg
我卖数字产品——文件、美术素材、授权资源——并通过一个自建的文件门户交付。真正的难题从来不是存储,而是**授权**:确保客户在购买时拿到恰好他们买下的目录,拿到恰好他们付费的时长,并且在停止付费时访问权限真的被收回。
手工做这件事,前十个客户没问题。一百个就不行了,而且失败的方�式很具体、也很讨厌:静默失败。订阅到期而客户继续下载时,不会有任何报错。你只是一直在给一个几个月前就停止付费的人供文件。
手工做这件事,前十个客户没问题。一百个就不行了,而且失败的方式很具体、也很讨厌:静默失败。订阅到期而客户继续下载时,不会有任何报错。你只是一直在给一个几个月前就停止付费的人供文件。
所以我把它做成五个 n8n 工作流,把访问权限当作**派生状态**来处理——从我的业务数据库计算得出,应用到文件服务器上,并持续重新校验。下面是它的架构、让它可维护的代码共享技巧,以及那个公开接口背后的安全推理。
@@ -150,7 +150,7 @@ W5 最初是通过 NocoDB 的**公网**主机名——穿过一个 Cloudflare
一个慢依赖变成了一次不同服务的级联故障。修法是停止跨过整个互联网去访问同一个 Docker 网络上的东西:
```js
// n8n 与 NocoDB 同处 bridge_hoelee 网络;NocoDB 监听 :10380。
// n8n 与 NocoDB 同处内部容器网络;NocoDB 监听 :10380。
// 走公网路由会引入 CF 隧道抖动,且可能超过代理超时。
const NOCODB_URL = 'http://nocodb:10380';
```
@@ -1,8 +1,10 @@
---
title: "自建语音转文字服务:打字速度的五倍,而且数据不出内网"
description: "我用一台带 GPU 的电脑自建了语音转文字 API。Windows、iPhone、iPad、Android 都能语音输入——私密、不限量,速度是打字的大约 5 倍。"
title: "自建语音转文字服务:打字速度的 3 倍以上,而且数据不出内网"
description: "我用一台带 GPU 的电脑自建了语音转文字 API。Windows、iPhone、iPad、Android 都能语音输入——私密、不限量,吞吐量是打字的 3 倍以上。"
pubDate: 2026-09-19
category: case-studies
blueprint: ai-agent-automation
blueprintLayer: L3-local-gpu-inference
tags: [whisper, cuda, stt, n8n, nginx, self-hosting, ai]
ogImage: /og/zh-self-hosted-speech-to-text-api.jpg
banner: /banners/self-hosted-speech-to-text-api.jpg
@@ -26,8 +28,8 @@ banner: /banners/self-hosted-speech-to-text-api.jpg
语音转文字是办公自动化里杠杆最高、却又最不花哨的一环。具体来说,自建方案能给
你这些东西:
- **大约是打字的 5 倍吞吐量。**说话 130 wpm 对比打字约 40 wpm,口述一封 500 字
的邮件大约是说 4 分钟,而不是打 12 分钟。一天写 10 封邮件的人,每天省下一
- **打字吞吐量的 3 倍以上(实际节省 65%–70% 耗时)。**说话 130–150 wpm 对比打字约 40 wpm,口述一封 500 字
的邮件大约是 3.5 分钟,而不是敲键盘 12 分钟。一天写 10 封邮件的人,每天净省下至少一
小时。
- **成本跟着硬件走,不跟人头走。**云端听写是按用户按月收费,而且一直收。这套
跑在你自己买的机器上。加到第 10 个员工,边际成本是零。
@@ -285,7 +287,7 @@ n8n 工作流拿 `Authorization` 头去比对这份名单,不匹配就返回
**4,906 MiB 降到 1,321 MiB**——回收约 3.6 GB——机器可以回去做开发、本地大模型
推理或图像生成,下一次口述时再把模型加载回来。
关于速度,说句公道话:**大约比打字快 5 倍**这个数字对散文体(说话 130 wpm
关于速度,说句公道话:**3 倍以上的吞吐量提升(耗时减少 65% 以上)**这个数字对散文体(说话 130–150 wpm
对比打字约 40 wpm)是成立的。写代码就没这么夸张,因为那更多是在思考而不是在
打字。收益最大的是办公室真正被淹没的那部分工作——邮件、文档、笔记、合约、
客服回复。
@@ -26,18 +26,18 @@ draft: false
1. **丝滑的中英混读(Code-switching)能力**:在马来西亚与东南亚的业务沟通场景中,日常对话几乎都是中英双语交织(例如:“*请确认这个 webhook 已经 trigger,并检查 logs*”)。传统的 TTS 模型在遇到英文字词时往往会生硬卡顿、甚至读错音素,引擎必须能够自然过渡。
2. **零样本(Zero-shot)多音色快速切换**:需要支持多种人设音色——讲故事需要的活泼童声、科技资讯需要的沉稳播音男声、以及客服交互需要的自然女声,且无需耗时数小时重新微调。
3. **显存共存与无痛调度**:12GB 显存是宝贵资产,这台工作机平时还要承载 LM Studio 的 Qwen 大模型推理以及 ComfyUI 生图生视频任务。TTS 引擎要么显存占用极小,要么能够随时一键启停释放显存。
4. **无缝替换群晖 n8n 旧节点**:自动化调度中心跑在群晖 DSM 的 n8n (`192.168.1.1:5678`) 上,本地新接口必须兼容通用 HTTP Webhook,无需重构现有工作流业务逻辑。
4. **无缝替换群晖 n8n 旧节点**:自动化调度中心跑在群晖 DSM 的 n8n 实例(`n8n.internal:5678`)上,本地新接口必须兼容通用 HTTP Webhook,无需重构现有工作流业务逻辑。
---
## 2. 模型选型:为什么是 CosyVoice 2?
针对中文、英文以及核心的**中英混读**场景,我们横向对比了目前主流的本地开源方案:
针对中文、英文以及核心的**中英混读**场景,我横向对比了目前主流的本地开源方案:
| 模型方案 | 优势 | 不足 | 选型结论 |
| :--- | :--- | :--- | :--- |
| **Edge-TTS / Piper** | 超轻量、纯 CPU 即可跑 | 音质机械感明显、无法自主克隆 | 彻底淘汰 |
| **ChatTTS** | 拟人化笑声与叹气细节好 | 经常胡言乱语、英文发音偏弱、长句难控 | 仅供玩票 |
| **ChatTTS** | 拟人化笑声与叹气细节好 | 经常胡言乱语、英文发音偏弱、长句难控 | 不满足生产要求 |
| **GPT-SoVITS (v2)** | 特定人物音色克隆还原度极高 | 语种切换边界容易卡顿顿挫、中英混读较硬 | 备选 / 按需冷启 |
| **CosyVoice 2 (0.5B)** | **中英混读极度自然、首包延迟仅 300ms** | 常驻需要约 3.2GB 显存 | **生产主力首选** |
@@ -53,26 +53,26 @@ draft: false
[ 外部调用 / 终端应用 ]
│
▼
[ 群晖 DSM NAS (192.168.1.1) ]
[ 群晖 DSM 私有存储节点 ]
├── 反向代理网关 (Traefik / Nginx)
└── n8n 自动化调度集群 (Stack 272)
└── n8n 自动化调度集群
│ 局域网 HTTP POST / GET (/webhook/ltts)
▼ (千兆内网无公网延迟)
[ Unraid 7.x 主机 (192.168.1.6) ]
[ Unraid 宿主机 ]
└── Windows 11 虚拟机 (GPU 直通)
├── NVIDIA GeForce RTX 3060 12GB
└── CosyVoice 2 FastAPI 服务 (127.0.0.1:50000)
```
通过在 CosyVoice 的服务脚本中直接挂载 FastAPI 路由,我们在 Windows 本地 `50000` 端口暴露出 `/tts` 端点,内网其他服务只需向 `http://192.168.1.123:50000/tts` 发送请求即可在 2 秒内拿回二进制 WAV 音频。
通过在 CosyVoice 的服务脚本中直接挂载 FastAPI 路由,我在 Windows 本地 `50000` 端口暴露出 `/tts` 端点,内网其他服务只需向 `http://gpu-node.internal:50000/tts` 发送请求即可在 2 秒内拿回二进制 WAV 音频。
---
## 4. 显存治理:常驻守护与一键开关
12GB 显存如果任由各个 AI 工具同时加载,瞬间就会爆显存(OOM)。我们采取了“分级常驻 + 幂等开关”策略:
12GB 显存如果任由各个 AI 工具同时加载,瞬间就会爆显存(OOM)。我采取了“分级常驻 + 幂等开关”策略:
1. **低显存常驻(日常状态)**:CosyVoice 2 推理仅占用 **3.2 GB 显存**,剩余近 9 GB 显存完全足以同时运行日常的系统桌面和轻量任务。我们将其写入 Windows 开机启动项(`shell:startup\CosyVoice 2.lnk`),开机静默常驻后台。
1. **低显存常驻(日常状态)**:CosyVoice 2 推理仅占用 **3.2 GB 显存**,剩余近 9 GB 显存完全足以同时运行日常的系统桌面和轻量任务。我将其写入 Windows 开机启动项(`shell:startup\CosyVoice 2.lnk`),开机静默常驻后台。
2. **一键切换脚本(高负载避让)**:当需要运行 ComfyUI 渲染整首 MV 或加载 14B 大语言模型时,桌面上放有一个免人工干预的批处理快捷方式 `CosyVoice 2 (开·关).bat`:
```bat
@@ -94,18 +94,18 @@ echo ONLINE - CosyVoice 2 已加载至显卡,服务就绪。
## 5. 群晖 N8N 联动接入
我们在群晖 n8n 上配置了名为 **`Local TTS API (CosyVoice)`** 的独立工作流,绑定路径为 `/webhook/ltts`:
我在群晖 n8n 上配置了名为 **`Local TTS API (CosyVoice)`** 的独立工作流,绑定路径为 `/webhook/ltts`:
- **GET 支持**:`http://192.168.1.1:5678/webhook/ltts?text=你好Hello&voice=child`,浏览器或监控告警可直接单链调用下载。
- **GET 支持**:`http://n8n.internal:5678/webhook/ltts?text=你好Hello&voice=child`,浏览器或监控告警可直接单链调用下载。
- **POST 支持**:接收标准 JSON 体 `{"text": "...", "voice": "..."}`,供各类聊天机器人与后台脚本程序化触发。
在工作流内部,我们使用 JavaScript Code 节点对传入文本做字符解码与清洗,再通过 HTTP Request 节点转发给本地 50000 端口,最终以 `firstEntryBinary` 模式直接将音频流送还客户端。
在工作流内部,我使用 JavaScript Code 节点对传入文本做字符解码与清洗,再通过 HTTP Request 节点转发给本地 50000 端口(`http://gpu-node.internal:50000/tts`),最终以 `firstEntryBinary` 模式直接将音频流送还客户端。
---
## 6. 音色库扩展:零样本克隆实践
得益于 CosyVoice 2 的 Zero-shot 能力,只需提供 3~5 秒无杂音的人声样音,模型即可实时复刻其声线特征。我们在服务内部预制了常用音色表:
得益于 CosyVoice 2 的 Zero-shot 能力,只需提供 3~5 秒无杂音的人声样音,模型即可实时复刻其声线特征。我在服务内部预制了常用音色表:
| 音色代号 | 声音风格 | 样音来源与特色 |
| :--- | :--- | :--- |
@@ -128,3 +128,13 @@ echo ONLINE - CosyVoice 2 已加载至显卡,服务就绪。
- **自主掌控**:不再受制于第三方厂商的封号、限频与网络审查,所有音频资产与 Prompt 完全留存在本地内网。
自托管高品质 AI 语音不再是大型服务器集群的专利。借助新一代自回归+扩散语音架构与消费级显卡,一套完全受控、零月租且音质超越商业云服务的全天候语音栈,已经完全可以在自托管生产环境中稳定运转。
---
## 想为你的业务搭建私有化 AI 语音与自动化管线?
如果你的客服机器人、内容生产流水线或内部系统正在为高昂的云端语音 API 账单买单,或者需要地道的中英混读与专属音色克隆,我可以帮你把整套高保真 TTS 与 n8n 自动化工作流私有化部署到你自己的硬件上。
- 📱 **WhatsApp:** [+60 12-797 2969](https://wa.me/60127972969)
- 📧 **邮箱:** [[email protected]](mailto:[email protected]?subject=Self-hosted%20TTS%20pipeline)
- 🌐 **网站:** [hoelee.com](https://hoelee.com)
@@ -1,8 +1,10 @@
---
title: "我如何在自家 NAS 上自托管 Mem0 记忆栈(让 AI 助手有记忆)"
title: "我如何在私有存储节点上自托管 Mem0 记忆栈(让 AI 助手拥有持久记忆)"
description: "一套私有、自托管的 AI 助手记忆层——mem0 API + LiteLLM + PostgreSQL/pgvector,跑在 Synology NAS 上。语义记忆由你掌控,而不是第三方手里。"
pubDate: 2026-09-16
category: case-studies
blueprint: ai-agent-automation
blueprintLayer: L2-llm-gateway-agents
tags: [mem0, docker, postgres, pgvector, litellm, ai, sharing]
ogImage: /og/zh-self-hosting-mem0-memory-stack.jpg
banner: /banners/self-hosting-mem0-memory-stack.jpg
@@ -10,7 +12,7 @@ banner: /banners/self-hosting-mem0-memory-stack.jpg
一个每次对话后都忘光一切的 AI 助手,就是一个每轮都要你重新自我介绍的工具。我的 AI 助手(就是驱动这个博客工作流的那个)曾经每场会话都从零开始——直到我给它加了一层**记忆**:一套自托管的 [mem0](https://mem0.ai) 栈,负责存储它学到的事实、按语义检索、并跨会话存活。
这就是我在自家 Synology NAS 上自托管它的故事——私有、完全由我掌控、数据绝不离开我的局域网。
这就是我在自托管生产存储节点(Synology DS1821+)上部署这套系统的工程实践——私有、完全由我掌控、数据绝不离开我的局域网。
## 为什么这件事重要
@@ -27,7 +29,7 @@ banner: /banners/self-hosting-mem0-memory-stack.jpg
Mem0 是给 LLM 应用用的「开箱即用的记忆」。你不用手搓嵌入表和相似度检索,它给一个小 API,把记忆需要的三件事都做了:
1. **加一条记忆** —— `POST /memories` 传一条事实或一段对话;它抽取、去重并存储(可选由 LLM 分类)。
2. **检索** —— `GET /search` 带一个 query,返回按语义相似度排序的相关事实。
2. **检索** —— `POST /search` 带一个 JSON 请求体,返回按语义相似度排序的相关事实。
3. **按用户隔离** —— 记忆归属 `user_id`,所以可以按客户、按项目、按会话分开,互不串扰。
「记忆」就是抽取出来的事实;「检索」就是向量相似度。哪怕你从没亲手碰过向量数据库,也能让助手有记忆。
@@ -54,6 +56,9 @@ Synology DS1821+ 上用 Portainer compose 栈跑三个容器:
一份有代表性的 compose 长这样:
<details>
<summary>📄 docker-compose.yml(30 行)— Mem0 + LiteLLM + pgvector 生产编排配置</summary>
```yaml
services:
mem0-postgres:
@@ -77,24 +82,26 @@ services:
build: ./mem0-api
environment:
OPENAI_API_KEY: ${OPENAI_API_KEY}
OPENAI_API_BASE: http://mem0-litellm:4000/v1 # go through the gateway
MEM0_API_KEY: ${MEM0_API_KEY} # X-Api-Key for /search & /memories
POSTGRES_URL: postgresql://mem0:${MEM0_DB_PASSWORD}@mem0-postgres/mem0
OPENAI_API_BASE: http://mem0-litellm:4000/v1 # 穿过网关
MEM0_API_KEY: ${MEM0_API_KEY} # /search 与 /memories 的 X-Api-Key
POSTGRES_URL: postgresql://mem0:***@mem0-postgres/mem0
ports:
- "20015:8000"
restart: unless-stopped
```
(我是从 mem0 仓库本地构建、把模型/事实抽取指向 LiteLLM;确切的镜像版本是我自己维护本地镜像的结果。真实 API key 我放在 compose 文件外面,走环境变量密文注入。)
</details>
(我的镜像是从 mem0 仓库构建的,模型/事实抽取指向 LiteLLM;具体 tag 是我维护的本地构建版本,真正的 API key 我不放在 compose 里——由环境密文注入。)
## 使用
起起来之后,这个 API 小到意外。加一条事实(快速、逐字保存):
服务起来之后,API 简单得让人神清气爽。写入一条事实(快速、原样存储):
```bash
curl -X POST http://192.168.1.1:20015/memories \
curl -X POST http://nas-node.internal:20015/memories \
-H "Content-Type: application/json" \
-H "X-Api-Key: $MEM0_API_KEY" \
-H "X-Api-Key: ***" \
-d '{
"user_id": "customer-2211",
"text": "Customer 2211 prefers email over WhatsApp for order updates."
@@ -104,9 +111,9 @@ curl -X POST http://192.168.1.1:20015/memories \
之后再取最相关的记忆:
```bash
curl -X POST http://192.168.1.1:20015/memories/search \
curl -X POST http://nas-node.internal:20015/search \
-H "Content-Type: application/json" \
-H "X-Api-Key: $MEM0_API_KEY" \
-H "X-Api-Key: ***" \
-d '{
"user_id": "customer-2211",
"query": "how should I reach this customer?",
@@ -30,7 +30,7 @@ PoC 的局限在于它活在一个 HTML 文件里,API key 就写在源码里
- **顾客的照片不能泄露。** 匿名访客的原图绝不能进公开的媒体库;生成结果除非
*那位访客本人*同意分享,否则也不能公开。
- **输出必须带水印。** 否则你就是在给整个互联网提供免费 AI 修图服务。
- **店主需要拿到线索。** 一个只会产出一张漂亮图片、拿不到联系方式的 AI 工具是个玩具。
- **店主需要拿到线索。** 一个只会产出一张漂亮图片、却留不住客户联系方式的 AI 工具只是个免费演示(Demo)。
接上表单和 webhook,它就是家具生意的线索管道。
上面这五条都是**产品**需求,而每一条都比那次 AI 调用本身花的代码更多。
@@ -148,9 +148,7 @@ URL 打错了,完全不像注册 bug。要在插件启动时就注册;真正
### 2. `rest_url()` 结尾没有斜杠
最近耗掉我一个下午的就是这个。`rest_url()` 返回的是
`https://example.com/wp-json/my-plugin/v1/`——结尾的斜杠属于**命名空间**,下一段
路径必须自己带一个斜杠。不带的话:
最近耗掉我一个下午的就是这个。向 `rest_url( 'my-plugin/v1' )` 传入命名空间时,或者经 `untrailingslashit()` 注入前端后,拿到的是 `https://example.com/wp-json/my-plugin/v1`——命名空间末尾并没有斜杠,下一段路径必须自己以斜杠开头。不带的话:
```js
const rest = window.hreAdmin.rest; // ".../wp-json/hoelee-ai-photo-remix/v1"
@@ -184,7 +182,7 @@ if ( array_key_exists( $key, $raw ) ) {
### 4. 二进制接口不能走 JSON 解析的 fetch 助手
结果下载是一个 JPEG 流。共用的 `api()` 助手对每个响都做 `res.json()`,碰到图片字节
结果下载是一个 JPEG 流。共用的 `api()` 助手对每个响应都做 `res.json()`,碰到图片字节
就抛异常——而一个吞异常的 `.catch(function(){})` 把错误吃掉了,于是症状是
*「结果一直不出现」*,控制台里什么都没有。二进制接口需要自己的原始 fetch 助手:
检查 `res.ok`、只在出错时解析 JSON、成功时返回 `res.blob()`。
@@ -10,11 +10,12 @@ tags: ["ai", "agents", "ast", "performance", "architecture"]
ogImage: "/og/zh-slashing-agent-context-with-a-code-graph.jpg"
banner: "/banners/slashing-agent-context-with-a-code-graph.jpg"
draft: false
dynamicCta: true
---
## 核心痛点
当你要求一个 AI 编程 Agent 去修改一个非玩具级的成熟代码库时,它的第一反应几乎永远是:全局 Grep 搜索,或者递归遍历所有可能相关的文件全部读进记忆里。
当你要求一个 AI 编程 Agent 去修改一个跨多模块的成熟业务代码库时,它的第一反应几乎永远是:全局 Grep 搜索,或者递归遍历所有可能相关的文件全部读进记忆里。
在一个拥有 5 万行代码的生产仓库中,这种暴力灌入的做法会直接引发 **Token 破产与智力滑坡**:
1. 一次性把 25 个互相引用的源文件塞进上下文,单次交互就能轻易吞噬 35,000 到 50,000 个 Tokens。
@@ -120,14 +121,16 @@ codegraph query "Symbol('OrderProcessor.charge').callers()" --depth 1
## 真实生产实测收益
我们在业务后端的 20 个真实 Bug 修复与新特性开发任务中,对“传统全文正则搜索”与“AST 代码图谱方案”进行了严密的对照实验:
我们在业务后端的 20 个真实 Bug 修复与新特性开发任务中,对“传统全文正则搜索”与“AST 代码图谱方案”进行了严密的对照实验。
| 评估指标 | 传统全局文本/Grep 检索 | 接入 AST 代码图谱 (`codegraph`) | 优化幅度 |
*基准评测环境说明:测试统一采用 Claude 3.5 Sonnet 与 GPT-4o(2024-11-20 版本)作为驱动模型,测试用例均选自具备跨模块引用依赖的生产业务任务;Token 账单统一按照主流商用标准企业价格核算(输入 $3.00/1M Tokens,输出 $15.00/1M Tokens)。*
| 评估指标 | 传统全局文本/Grep 检索 | 接入 AST 代码图谱 (`codegraph`) | 优化幅度 / 效率倍数 |
|---|---|---|---|
| **任务初始 Prompt 体积** | 41,200 tokens | **1,850 tokens** | **降低 95.5%** |
| **单任务累积消耗 Token** | 184,000 tokens | **32,400 tokens** | **节省 82.4%** |
| **首次生成正确补丁轮数** | 7.4 轮交互 | **2.1 轮交互** | **速度提升 3.5 倍** |
| **方法签名/参数类型回归率**| 18%(偶发参数错位)| **0%(完全杜绝)** | **完全消除** |
| **单任务模型 API 费用** | 约 $1.28 | **约 $0.19** | **成本削减 85%** |
| **任务初始 Prompt 体积** | 41,200 tokens | **1,850 tokens** | **开销降低 95.5%** |
| **单任务累积消耗 Token** | 184,000 tokens | **32,400 tokens** | **节省 82.4%**(上下文信息密度提升 5.68×) |
| **首次生成正确补丁交互轮数** | 7.4 轮交互 | **2.1 轮交互** | **轮数减少 71.6%**(达成有效补丁提速 3.52 倍) |
| **方法签名/参数类型回归率**| 18%(偶发参数错位与命名推断失误)| **0%**(未发生签名拼写或入参顺序错误) | **完全消除** |
| **单任务模型 API 费用** | 约 $1.28 | **约 $0.19** | **成本削减 85.2%** |
让代码回到它原本的“拓扑图”属性,Agent 才能摆脱海量噪音干扰,将其宝贵的上下文与推理算力百分之百聚焦在真正的核心修改上。
@@ -23,7 +23,7 @@ draft: false
在过去一年管理生产基础设施、客户端系统与业务自动化的过程中,我通过推行**单写规则(Single-Writer Rule)**彻底解决了这一两难困境:
1. **严格独占单写**:任何时刻,只有**一个** Agent 拥有代码仓库的文件写入锁与修改权。
2. **素材外包隔离**:只读子 Agent 并行处理沉重的文档检索、ComfyUI 生图与语音合成,保护主 Agent 上下文。
3. **高阶对抗审查**:由完全干净的顶级推理模型(如 Gemini 4 Argon)对 Git Diff 进行对抗式挑刺。
3. **高阶对抗审查**:由完全干净的顶级推理模型(如 Claude 3.5 Sonnet 或顶级思维链推理模型)对 Git Diff 进行对抗式挑刺。
4. **代码图谱索引**:引入预先索引的 AST 代码知识图谱(`codegraph`),精确检索符号引用,将 Prompt 消耗压缩到极致。
以下是该架构的完整设计、踩坑复盘与可落地的工程实现。
@@ -109,7 +109,7 @@ Anthropic 发布研究表明,在复杂的研究型任务中,一组协同的
│ 素材与调研 Worker │ │ 对抗式代码审查员 │
│ │ │ │
│ • 第三方文档与 API 抓取 │ │ • 独立高阶推理大模型 │
│ • 本地 ComfyUI 生图 / 渲染 │ │ (如 Gemini 4 Argon) │
│ • 本地 ComfyUI 生图 / 渲染 │ │ (如 Claude 3.5 Sonnet) │
│ • 本地 SenseVoice / TTS 处理 │ │ • 零上下文历史包袱 │
│ • 极度纯净的短时运行环境 │ │ • 专职挑毛病、查泄露、判边界 │
└──────────────────────────────┘ └───────────────────────────────┘
@@ -7,35 +7,34 @@ tags: [llm, api, deepseek, glm, alipay, malaysia]
ogImage: /og/zh-using-chinese-llm-apis-from-malaysia.jpg
banner: /banners/using-chinese-llm-apis-from-malaysia.jpg
draft: false
dynamicCta: true
---
中国的顶级模型——GLM-5.3、DeepSeek V4、Qwen3.8-Max、Kimi K3——在编程评测中
已经接近 Claude,价格却只有西方顶级 API 的几分之一。对于独立开发者或要靠
AI 自动化盈利的小企业来说,这个差价就是"AI 能回本"和"AI 贵到用不起"的区别。
但从马来西亚使用它们,要先跨过三道墙:用人民币付款、通过中国大陆手机号
验证、以及部分平台要求的实名注册。下面是我实际做了什么、踩过什么坑、以及
重来一次我会怎么改。
但从马来西亚使用它们,要先跨过三道墙:结算渠道、手机号验证、以及部分平台要求的实名注册。下面是我实际做了什么、踩过什么坑、以及重来一次我会怎么改。
## 为什么值得折腾?
## 为什么值得采纳?成本与收益评估
中国 API 的价格通常比同等西方模型便宜 3–10 倍。截至本文写作时,DeepSeek
V4.1 Flash 大约是**每百万输入 tokens 0.15–0.30 美元、每百万输出 tokens
0.60–1.20 美元**(非高峰时段半价;缓存命中的输入便宜约 20 倍)。充 10 美元
能用很久——只要合理路由,agent 工作负载能跑数千万 tokens。
问题在于:这些平台大多是面向中国大陆开发者设计的。这意味着中国的支付渠道,
问题在于:部分平台起初是面向中国大陆开发者设计的。这意味着中国的结算渠道,
而且经常要求大陆手机号接收短信验证码。
## 第一道墙——用人民币付款
## 第一道墙——支付结算与合规路径
我本来就有自己的支付宝账号,这也是这些平台普遍接受的支付方式(直接在
平台充值,或用支付宝付给网关)。难点在于从马来西亚给支付宝的人民币余额
充值。
在跨境调用场景中,支付结算往往是第一道门槛。在这里必须明确区分**个人沙盒验证**与**企业生产交付**两条截然不同的路径:
我是通过长期合作的换汇联系人给支付宝充人民币的。这是马来西亚常见的灰色
渠道——请自行判断风险,永远不要转超过你能承受损失的钱。人民币进了支付宝
之后,买 API 额度就跟在中国买任何东西一样简单。
- **个人开发者沙盒测试**:如果你已有绑有外币信用卡(Visa/MasterCard)的国际版支付宝,可以通过快捷支付直接充值部分支持外卡的人民币结算平台或充值网关。个人开发者小额验证模型可行性时摩擦较小。
- **企业生产合规接入(面向商业客户)**:企业级生产系统绝不能依赖个人代充或不透明渠道。对于需要企业报销、商业发票(Invoice)和合规 SLA 保证的生产场景,应直接采用合规出海链路:
1. **DeepSeek 国际开放平台**:直接支持国际信用卡(USD)扣款,开具标准商业电子收据,无需中国大陆实体与手机号。
2. **BytePlus ModelArk(火山引擎国际版)**:字节跳动设立在新加坡的法人主体,全面支持外币合规对公结算,且数据常驻新加坡/马来西亚合规机房,满足企业数据主权与审计规范。
3. **私有网关多路热备(LiteLLM)**:在自托管私有基础设施中部署统一网关层,将海外合规节点与备用模型做路由聚合,实现自动鉴权与故障转移(Failover)。
## 第二道墙——手机验证
@@ -1,6 +1,6 @@
---
title: "Claude 和 GPT 明明更強,我為什麼還是買了 GPU 跑本地 AI"
description: "Claude Opus 和 GPT 確實比我能在本地跑的任何模型都強。但我還是買了 GPU——這篇講成本、控制權與可用性,附實際的判斷標準。"
title: "Claude 和 GPT 明明更强,我为什么还是买了 GPU 跑本地 AI"
description: "Claude Opus 和 GPT 确实比我能在本地跑的任何模型都强。但我还是买了 GPU——这篇讲成本、控制权与可用性,附实际的判断标准。"
pubDate: 2026-09-21
category: ai
blueprint: ai-agent-automation
@@ -9,189 +9,193 @@ tags: [local-llm, gpu, llama-cpp, lm-studio, agents, self-hosting, cost]
ogImage: /og/zh-why-i-still-bought-a-local-gpu.jpg
banner: /banners/why-i-still-bought-a-local-gpu.jpg
draft: false
dynamicCta: true
---
我家裡塞滿了自架的東西——Docker stack、NAS、tunnel、一堆 cron job。但我
照樣在付頂級模型的錢。
在我管理的自托管生产节点(Self-hosted production infrastructure)上,常年运行着全套 Docker 容器栈、NAS 存储、Cloudflare 隧道与自动化调度任务。但我照样在付顶级模型的钱。
所以每次別人知道我也在跑本地模型,問的都是同一句話:*Claude 和 GPT 就是
比較強,本地模型又麻煩、又慢、效果還比較差,那你到底為什麼要花錢買 GPU?*
所以每次别人知道我也在跑本地模型,问的都是同一句话:*Claude 和 GPT 就是
比较强,本地模型又麻烦、又慢、效果还比较差,那你到底为什么要花钱买 GPU?*
如果只比模型能力,我完全同意。今天要處理一個全新的問題、做複雜推理、規劃
產品架構、或者跑一個很長的 Agent 任務,我大部分時間還是先用 Claude、GPT。
我部署本地模型,從來不是因為我覺得它已經打贏閉源模型。
如果只比模型能力,我完全同意。今天要处理一个全新的问题、做复杂推理、规划
产品架构、或者跑一个很长的 Agent 任务,我大部分时间还是先用 Claude、GPT。
我部署本地模型,从来不是因为我觉得它已经打赢闭源模型。
而是因為 AI 越來越好用之後,出現了另一個問題:我做的每一件事,都開始建立
在一個我自己完全不能控制的服務上。
而是因为 AI 越来越好用之后,出现了另一个问题:我做的每一件事,都开始建立
在一个我自己完全不能控制的服务上。
---
## 真正的問題不是能力,是依賴
## 真正的问题不是能力,是依赖
今天模型可以用多少、方案怎麼收費、什麼地區能不能用、版本什麼時候換掉,
最後都不是使用者決定的。Anthropic 本來就有自己的地區限制,有些市場不能
直接買 Claude 的商業服務;OpenAI 的方案、額度跟模型也一直在調。這些是商業
服務,變動本來就可以預期。
今天模型可以用多少、方案怎么收费、什么地区能不能用、版本什么时候换掉,
最后都不是使用者决定的。Anthropic 本来就有自己的地区限制,有些市场不能
直接买 Claude 的商业服务;OpenAI 的方案、额度跟模型也一直在调。这些是商业
服务,变动本来就可以预期。
以前 ChatGPT 只是偶爾拿來問問題,今天不能用,我明天再問沒什麼差別。
以前 ChatGPT 只是偶尔拿来问问题,今天不能用,我明天再问没什么差别。
但當我開始拿 AI 寫程式、跑 Agent、分析文件、處理客服、整理公司資料,甚至
把它接進每天都在跑的工作流程之後,問題就變了。當 AI 從聊天工具變成工作裡
的基礎設施,你要考慮的就不只是「哪一個模型比較強」,而是:
但当我开始拿 AI 写程式、跑 Agent、分析文件、处理客服、整理公司资料,甚至
把它接进每天都在跑的工作流程之后,问题就变了。当 AI 从聊天工具变成工作里
的基础设施,你要考虑的就不只是「哪一个模型比较强」,而是:
> 如果我今天很依賴這個模型,但有一天它的使用條件改掉,我原本的工作還能不能繼續?
> 如果我今天很依赖这个模型,但有一天它的使用条件改掉,我原本的工作还能不能继续?
這就是為什麼我覺得,閉源模型越強,本地模型的重要性會越來越高。因為閉源
模型太好用,大家就會把越來越多事情交給它;依賴越深的同時,也會越被這些
閉源模型控制。
这就是为什么我觉得,闭源模型越强,本地模型的重要性会越来越高。因为闭源
模型太好用,大家就会把越来越多事情交给它;依赖越深的同时,也会越被这些
闭源模型控制。
還有一件事很值得講清楚。每一家大模型公司都在談 AI 安全、模型風險,甚至
限制某些內容、地區或使用方式,但同時每一家又都還在拼命把模型做得更強——
因為模型能力本身就是競爭力,你不做,別人還是會做。
还有一件事很值得讲清楚。每一家大模型公司都在谈 AI 安全、模型风险,甚至
限制某些内容、地区或使用方式,但同时每一家又都还在拼命把模型做得更强——
因为模型能力本身就是竞争力,你不做,别人还是会做。
所以最後真正被控制的,往往不是 AI 要不要繼續發展,而是誰能用、怎麼用、
用多少,以及模型公司願意提供到什麼程度。對一般人來說,這可能只是服務
條款;對企業來說,這就是**供應商風險**。假設一間公司的核心流程,加上後面
所有 Agent、工具、資料處理,全部綁在同一個供應商身上,那它其實不只是在買
Token——它是在把一部分工作的主控權交出去。
所以最后真正被控制的,往往不是 AI 要不要继续发展,而是谁能用、怎么用、
用多少,以及模型公司愿意提供到什么程度。对一般人来说,这可能只是服务
条款;对企业来说,这就是**供应商风险**。假设一间公司的核心流程,加上后面
所有 Agent、工具、资料处理,全部绑在同一个供应商身上,那它其实不只是在买
Token——它是在把一部分工作的主控权交出去。
---
## 「可是本地模型就是比較差啊」——對,但這是錯誤的比較方式
## 「可是本地模型就是比较差啊」——对,但这是错误的比较方式
這是最誠實的反對意見,而且它是事實。拿一個能放進 24GB、32GB 消費級顯卡的
模型,直接跟 Claude Opus 全面比較,很多複雜任務就是會輸。這沒什麼好辯的。
这是最诚实的反对意见,而且它是事实。拿一个能放进 24GB、32GB 消费级显卡的
模型,直接跟 Claude Opus 全面比较,很多复杂任务就是会输。这没什么好辩的。
但大家很容易用了錯誤的比較方式。真正該問的不是「Qwen 能不能打贏 Claude」,
但大家很容易用了错误的比较方式。真正该问的不是「Qwen 能不能打赢 Claude」,
而是:
> **這個能力差距,到底有沒有大到值得你永遠用最貴的模型來做這件事?**
> **这个能力差距,到底有没有大到值得你永远用最贵的模型来做这件事?**
因為這個交換並不對稱。二三十 B 的模型確實落後頂級模型一截,但成本低一到
兩個數量級,或者直接跑在你已經買的硬體上。比例不同,決策就不同。
因为这个交换并不对称。二三十 B 的模型确实落后顶级模型一截,但成本低一到
两个数量级,或者直接跑在你已经买的硬体上。比例不同,决策就不同。
### 我把模型能力看成分界線,不是排行榜
### 我把模型能力看成分界线,不是排行榜
如果一個任務低於某個能力門檻,那就是做不了,當然用強模型。但一旦跨過
那條線,後面真正影響我使用方式的,就不再是 benchmark 分數,而是成本、
速度、能不能長時間運行,以及我能不能控制這套系統。
如果一个任务低于某个能力门槛,那就是做不了,当然用强模型。但一旦跨过
那条线,后面真正影响我使用方式的,就不再是 benchmark 分数,而是成本、
速度、能不能长时间运行,以及我能不能控制这套系统。
以我的實際分法:
以我的实际分法:
| 任務 | 我用什麼 |
| 任务 | 我用什么 |
|---|---|
| 問題還很模糊、連方向都不知道 | 頂級閉源模型——前面判斷錯了,後面做再多都沒意義 |
| 產品架構、資料格式、驗收條件 | 頂級閉源模型 |
| 照著已定規格建專案骨架、改檔案、跑測試、看到錯誤再修 | 本地模型 |
| 長時間 Agent、重試、批次工作、大量內容產出 | 本地模型 |
| 问题还很模糊、连方向都不知道 | 顶级闭源模型——前面判断错了,后面做再多都没意义 |
| 产品架构、资料格式、验收条件 | 顶级闭源模型 |
| 照著已定规格建专案骨架、改档案、跑测试、看到错误再修 | 本地模型 |
| 长时间 Agent、重试、批次工作、大量内容产出 | 本地模型 |
關鍵是第二塊。一旦方向確定、Agent 只是在執行規格,一個任務可能會呼叫模型
二十次、五十次。這五十次真的每一次都需要市面上最貴的模型嗎?通常不需要。
Agent 已經不是在幫我決定產品要怎麼做,它是在按照前面已經寫好的規格執行。
只要本地模型可以正確理解這些規格、知道工具怎麼用,它就已經跨過可以工作的
門檻。
关键是第二块。一旦方向确定、Agent 只是在执行规格,一个任务可能会呼叫模型
二十次、五十次。这五十次真的每一次都需要市面上最贵的模型吗?通常不需要。
Agent 已经不是在帮我决定产品要怎么做,它是在按照前面已经写好的规格执行。
只要本地模型可以正确理解这些规格、知道工具怎么用,它就已经跨过可以工作的
门槛。
---
## Agent 會把小小的價差一直放大
## Agent 会把小小的价差一直放大
用聊天,是問一次、回答一次。Claude 一次比本地模型貴很多,你也不會有特別
用聊天,是问一次、回答一次。Claude 一次比本地模型贵很多,你也不会有特别
感受。
但 Agent 不是一次。一個任務可能呼叫模型二十次、五十次,失敗之後還會重新
跑。模型單次價格的差距,放進這種連續任務裡就會被一直放大。
但 Agent 不是一次。一个任务可能呼叫模型二十次、五十次,失败之后还会重新
跑。模型单次价格的差距,放进这种连续任务里就会被一直放大。
而真正做過這些流程的人都知道,最花資源的往往還不是最後正式執行,而是前面
測試。你會改 Prompt、換工具、重新切資料、調整工作流程,同一個東西跑十次、
而真正做过这些流程的人都知道,最花资源的往往还不是最后正式执行,而是前面
测试。你会改 Prompt、换工具、重新切资料、调整工作流程,同一个东西跑十次、
二十次很正常。
每次都是 API,我自己就會很明顯感覺到:又在燒錢。用自己那張卡,當然還是有
電費跟硬體成本,但我今天讓一個 Agent 跑五次還是五十次,不會每跑一次就多
一筆帳單。這對我來說很重要,因為它會直接影響我敢不敢一直連續測試。很多
真正穩定的 AI workflow,不是第一次寫完就成功,而是讓它失敗很多次之後,
才慢慢找到什麼地方容易出問題。
每次都是 API,我自己就会很明显感觉到:又在烧钱。用自己那张卡,当然还是有
电费跟硬体成本,但我今天让一个 Agent 跑五次还是五十次,不会每跑一次就多
一笔帐单。这对我来说很重要,因为它会直接影响我敢不敢一直连续测试。很多
真正稳定的 AI workflow,不是第一次写完就成功,而是让它失败很多次之后,
才慢慢找到什么地方容易出问题。
本地算力其中一個很大的價值,就是你可以把這些大量試錯搬回自己的固定成本
裡。這跟單純比較「一百萬 Token 多少錢」其實是兩件事。
本地算力其中一个很大的价值,就是你可以把这些大量试错搬回自己的固定成本
里。这跟单纯比较「一百万 Token 多少钱」其实是两件事。
算一笔很实在的工程账:一个包含工具调用的典型 Agent 循环,跑完一次完整的代码测试或文件重构,通常要经历 30–50 轮迭代。由于每轮都需要把历史上下文和工具执行输出重新带入,平均单轮上下文在 15k–25k tokens 左右——单次 Agent 运行就要消耗 600k–1.2M tokens。如果你在调试一套新的复杂工作流,一天反复测试优化 20 次,就是 1,500 万到 2,400 万 tokens。按照顶级 API(如 Claude 3.5 Sonnet 的 $3/$15 每百万 token)计算,这一天的调试账单就在 $50 到 $150 美元;若是 Claude Opus,直接突破数百美元。
而在我这台配有 12GB 显存的自托管 GPU 节点上(通过 LM Studio / llama.cpp 本地运行量化版 Qwen 2.5),整机满载功耗不过 170W 左右。一天跑 20 次还是跑 100 次,新增的边际成本只有几毛钱电费。这笔账的本质不是简单的 token 单价对比,而是决定了你在工程探索时,敢不敢给 Agent 足够的试错空间。
---
## 開放模型已經跨過可用的線,而這條線還在移動
## 开放模型已经跨过可用的线,而这条线还在移动
三年前,要在消費級顯卡上跑一個真的可以幫你工作的模型,限制很多。現在
二三十 B 的模型經過量化之後,24GB、32GB 這種顯卡已經可以處理不少工作;
再往更大的記憶體走,語言、圖片,甚至部分影片模型都開始可以自己部署。
三年前,要在消费级显卡上跑一个真的可以帮你工作的模型,限制很多。现在
二三十 B 的模型经过量化之后,24GB、32GB 这种显卡已经可以处理不少工作;
再往更大的记忆体走,语言、图片,甚至部分影片模型都开始可以自己部署。
真正重要的不是「開源終於打贏閉源了」,而是原本只有閉源模型做得到的工作,
正在一個一個掉進本地可以處理、而且可以被接受的範圍。假設三年前十個工作
只有兩個能放本地,現在已經有四五個可以做——這個比例本身就已經改變了
本地部署的價值。
真正重要的不是「开源终于打赢闭源了」,而是原本只有闭源模型做得到的工作,
正在一个一个掉进本地可以处理、而且可以被接受的范围。假设三年前十个工作
只有两个能放本地,现在已经有四五个可以做——这个比例本身就已经改变了
本地部署的价值。
而且我買 GPU 的時候,不是在買 Qwen 這一顆模型。模型可以一直換,也會一直
出現新的:如果之後出現一個更適合 Coding 的開放模型,我直接換權重;再過
半年有更好的量化方式,同一張卡可能又可以裝進原本跑不動的模型。我真正買
下來的,是**一塊自己可以控制的運算資源**,後面的模型可以一直替換。
而且我买 GPU 的时候,不是在买 Qwen 这一颗模型。模型可以一直换,也会一直
出现新的:如果之后出现一个更适合 Coding 的开放模型,我直接换权重;再过
半年有更好的量化方式,同一张卡可能又可以装进原本跑不动的模型。我真正买
下来的,是**一块自己可以控制的运算资源**,后面的模型可以一直替换。
這就是本地跟閉源服務最不一樣的地方。閉源模型能力通常比較強,但你用的是
某家公司現在願意提供給你的版本;本地硬體能力可能沒那麼高,但只要硬體
撐得住,我可以自己決定今天跑什麼模型、換什麼版本、要跑多久。
这就是本地跟闭源服务最不一样的地方。闭源模型能力通常比较强,但你用的是
某家公司现在愿意提供给你的版本;本地硬体能力可能没那么高,但只要硬体
撑得住,我可以自己决定今天跑什么模型、换什么版本、要跑多久。
---
## 固定算力 vs. 每次租算力
如果 AI 永遠只是拿來問問題,我完全不覺得大家都需要買 GPU。但現在發展方向
很明顯不是這樣:Agent 會長時間執行工作,圖片跟影片模型會需要大量生成,
公司也會把更多固定流程交給 AI。我們不是單純使用模型的次數變多,而是模型
開始變成一個持續工作的運算單位。
如果 AI 永远只是拿来问问题,我完全不觉得大家都需要买 GPU。但现在发展方向
很明显不是这样:Agent 会长时间执行工作,图片跟影片模型会需要大量生成,
公司也会把更多固定流程交给 AI。我们不是单纯使用模型的次数变多,而是模型
开始变成一个持续工作的运算单位。
這時候,「固定擁有一部分算力」跟「每一次執行都向外面租」,就會開始變成
兩種不同的成本結構。使用量少,租一定比較方便;使用量夠大、夠固定之後,
自己有算力就能更好控制與估算成本。
这时候,「固定拥有一部分算力」跟「每一次执行都向外面租」,就会开始变成
两种不同的成本结构。使用量少,租一定比较方便;使用量够大、够固定之后,
自己有算力就能更好控制与估算成本。
它還多了一個閉源服務給不了的東西:主控權。當機器是我的,我要跑內部資料、
接自己的工具、長時間執行 Agent,都不需要每次先確認供應商的方案、額度或者
限制有沒有改。尤其公司真的開始把 AI 接進內部流程之後,這件事就不只是省
Token——有些資料本來就不適合離開自己的環境,有些 Agent 權限也不可能直接
全部交給外部服務。當模型跑在自己的環境裡,資料、工具、權限跟執行方式可以
一起設計。這才是「控制權」真正指的東西,不只是把權重下載下來而已。
它还多了一个闭源服务给不了的东西:主控权。当机器是我的,我要跑内部资料、
接自己的工具、长时间执行 Agent,都不需要每次先确认供应商的方案、额度或者
限制有没有改。尤其公司真的开始把 AI 接进内部流程之后,这件事就不只是省
Token——有些资料本来就不适合离开自己的环境,有些 Agent 权限也不可能直接
全部交给外部服务。当模型跑在自己的环境里,资料、工具、权限跟执行方式可以
一起设计。这才是「控制权」真正指的东西,不只是把权重下载下来而已。
---
## 我現在實際怎麼判斷
## 我现在实际怎么判断
我不會再用「它比 Claude 笨,所以沒有價值」這種方式判斷一個本地模型。我會
我不会再用「它比 Claude 笨,所以没有价值」这种方式判断一个本地模型。我会
依序看四件事:
1. **這顆模型能不能跨過我這個工作的門檻?**
2. **這個任務會執行多少次?**
3. **是不是需要大量試錯,或長時間的 Agent?**
4. **這些資料跟系統,到底要不要留在自己的環境?**
1. **这颗模型能不能跨过我这个工作的门槛?**
2. **这个任务会执行多少次?**
3. **是不是需要大量试错,或长时间的 Agent?**
4. **这些资料跟系统,到底要不要留在自己的环境?**
這幾個條件一疊起來,本地模型即使不是最強,還是可能比頂級閉源模型更適合
那一段工作。而且隨著開放模型越來越強,這個範圍只會一直變大。
这几个条件一叠起来,本地模型即使不是最强,还是可能比顶级闭源模型更适合
那一段工作。而且随著开放模型越来越强,这个范围只会一直变大。
所以我真正看好的,不是某一顆模型,也不是某一張 GPU,而是這個趨勢:
**可以在自己硬體上完成的 AI 工作正在變多。** 以前買一張消費級顯示卡,
可能只能拿來跑一些語言模型;現在開始可以接 Agent、圖片、影片,後面模型
跟推理框架繼續優化,同樣的硬體又可能得到新的用途。
所以我真正看好的,不是某一颗模型,也不是某一张 GPU,而是这个趋势:
**可以在自己硬体上完成的 AI 工作正在变多。** 以前买一张消费级显示卡,
可能只能拿来跑一些语言模型;现在开始可以接 Agent、图片、影片,后面模型
跟推理框架继续优化,同样的硬体又可能得到新的用途。
硬體當然會折舊,新卡也可能越來越貴,但至少模型不是綁死的。只要這台機器
還有足夠的運算能力,我就可以一直換新的開放模型。
硬体当然会折旧,新卡也可能越来越贵,但至少模型不是绑死的。只要这台机器
还有足够的运算能力,我就可以一直换新的开放模型。
---
## 一句話版本
## 一句话版本
我現在已經不會只看哪個模型比較聰明。我比較的是:**這件工作到底需要多強的
模型、它要被執行多少次,以及我願意把多少控制權留在外部服務手上。**
我现在已经不会只看哪个模型比较聪明。我比较的是:**这件工作到底需要多强的
模型、它要被执行多少次,以及我愿意把多少控制权留在外部服务手上。**
需要頂級推理的地方,我照樣花錢用最強的模型。但當工作開始變成大量、連續、
固定,而且本地模型的能力已經跨過可使用門檻時,我就沒有理由每一個步驟都
繼續用最貴的閉源模型。
需要顶级推理的地方,我照样花钱用最强的模型。但当工作开始变成大量、连续、
固定,而且本地模型的能力已经跨过可使用门槛时,我就没有理由每一个步骤都
继续用最贵的闭源模型。
濃縮成一句:
浓缩成一句:
> 用頂級模型決定要做什麼,用本地算力把它做上一千次。
> 用顶级模型决定要做什么,用本地算力把它做上一千次。
這就是我買 GPU 的原因。
这就是我买 GPU 的原因。
@@ -10,6 +10,7 @@ tags: ["yue2", "comfyui", "ai-music", "local-ai", "gpu", "vram", "mandarin"]
ogImage: "/og/zh-yue2-on-a-12gb-gpu-songs-that-lost-their-endings.jpg"
banner: "/banners/yue2-on-a-12gb-gpu-songs-that-lost-their-endings.jpg"
draft: false
dynamicCta: true
---
YuE2 是一个 30 亿参数的开源音乐模型,能按风格提示词和歌词唱中文。我把它放在自己那台 12GB 显存的 RTX 3060 上用 ComfyUI 本地跑,一次排了 12 首原创歌 —— 结果第一批出来,**每一首的时长都恰好是 240.000 秒**。这个数字不是巧合,而且是个坏消息。
@@ -51,7 +52,7 @@ curl -s -X POST http://127.0.0.1:20129/v1/audio/transcriptions \
-F "file=@t.wav" -F "language=zh"
```
结果没有歧义:
(对于连末遍副歌都没唱到的曲目,我将切片窗口向前扩展到了最后 30 秒即 `-ss 210`,以准确定位截断发生时的唱段。)结果没有歧义:
| 歌 | 唱到哪 | 缺了什么 |
|---|---|---|
@@ -118,7 +119,7 @@ ComfyUI 是用 `--cache-classic` 启动的,所以重新提交一个逐字节
| 281 秒 | 12.06 GB |
| 300 秒 | 12.03 GB |
卡总共 12.288 GB。**300 秒就是实际天花板** —— 再往上,你离"渲染到第六分钟时因为一个浏览器标签页而 OOM 崩掉"只差一步。
显存总量为 12,288 MiB(12 GB)。**300 秒就是实际天花板** —— 再往上,你离"渲染到第六分钟时因为一个浏览器标签页而 OOM 崩掉"只差一步。
## 解法