Files
hoelee-blog/page.html
T
2026-10-20 05:53:59 +08:00

210 lines
29 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html><html lang="zh-Hans" data-theme="dark"> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta name="generator" content="Astro v5.18.2"><title>12GB 显卡跑本地 AI 视频:Wan 2.2 加 SeedVR2 的真实数据 — Mr Hoelee</title><meta name="description" content="在 RTX 3060 上实测:Wan 2.2 TI2V-5B 生成、SeedVR2 放大、1080p 分辨率陷阱,以及那条让连续五次失败全部通过的命令。"><link rel="canonical" href="https://blog.hoelee.com/posts/zh/running-ai-video-locally-on-a-12gb-gpu/"><!-- hreflang: this page + its other-language twin --><link rel="alternate" hreflang="en" href="https://blog.hoelee.com/posts/running-ai-video-locally-on-a-12gb-gpu/"><link rel="alternate" hreflang="zh-Hans" href="https://blog.hoelee.com/posts/zh/running-ai-video-locally-on-a-12gb-gpu/"><link rel="alternate" hreflang="x-default" href="https://blog.hoelee.com/posts/running-ai-video-locally-on-a-12gb-gpu/"><!-- Open Graph --><meta property="og:type" content="article"><meta property="og:site_name" content="Mr Hoelee"><meta property="og:title" content="12GB 显卡跑本地 AI 视频:Wan 2.2 加 SeedVR2 的真实数据 — Mr Hoelee"><meta property="og:description" content="在 RTX 3060 上实测:Wan 2.2 TI2V-5B 生成、SeedVR2 放大、1080p 分辨率陷阱,以及那条让连续五次失败全部通过的命令。"><meta property="og:url" content="https://blog.hoelee.com/posts/zh/running-ai-video-locally-on-a-12gb-gpu/"><meta property="og:image" content="https://blog.hoelee.com/og/zh-running-ai-video-locally-on-a-12gb-gpu.jpg"><meta property="og:image:width" content="1200"><meta property="og:image:height" content="630"><meta property="og:locale" content="zh_CN"><meta property="og:locale:alternate" content="en_US"><!-- Twitter --><meta name="twitter:card" content="summary_large_image"><meta name="twitter:title" content="12GB 显卡跑本地 AI 视频:Wan 2.2 加 SeedVR2 的真实数据 — Mr Hoelee"><meta name="twitter:description" content="在 RTX 3060 上实测:Wan 2.2 TI2V-5B 生成、SeedVR2 放大、1080p 分辨率陷阱,以及那条让连续五次失败全部通过的命令。"><meta name="twitter:image" content="https://blog.hoelee.com/og/zh-running-ai-video-locally-on-a-12gb-gpu.jpg"><!-- Article-specific meta --><meta property="article:published_time" content="2026-10-20T00:00:00.000Z"><meta property="article:section" content="ai"><meta property="article:tag" content="ai-video"><meta property="article:tag" content="wan"><meta property="article:tag" content="comfyui"><meta property="article:tag" content="seedvr2"><meta property="article:tag" content="local-llm"><meta property="article:tag" content="gpu"><meta property="article:tag" content="cost"><!-- Favicon --><link rel="icon" href="/favicon.ico" sizes="any"><link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"><link rel="icon" type="image/png" sizes="192x192" href="/favicon-192x192.png"><link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png"><!-- Fonts are self-hosted via @fontsource (imported in frontmatter) --><!-- RSS --><link rel="alternate" type="application/rss+xml" title="Mr Hoelee — RSS" href="/rss.xml"><!-- Person / ProfilePage JSON-LD (E-E-A-T backbone) --><script type="application/ld+json">{"@context":"https://schema.org","@type":"Person","@id":"https://blog.hoelee.com/#person","name":"Lee Teong Hoe","alternateName":"Mr Hoelee","url":"https://blog.hoelee.com","email":"[email protected]","image":"https://blog.hoelee.com/images/lee-teong-hoe-640.jpg","sameAs":["https://www.linkedin.com/in/hoelee","https://github.com/hoelee","https://git.hoelee.com/hoelee","https://www.hoelee.com"],"jobTitle":"Full-stack Developer & DevOps Engineer","worksFor":{"@type":"Organization","name":"Hoelee Enterprise","url":"https://www.hoelee.com"},"knowsAbout":["Full-stack engineering","DevOps","Self-hosting","Docker","WordPress","AI automation","Web3"]}</script><!-- Theme init (before paint, no FOUC). Default = dark. --><script type="b370bf50f4daa8c28be67203-text/javascript">
(function () {
try {
var t = localStorage.getItem('theme');
if (t === 'light') {
document.documentElement.setAttribute('data-theme', 'light');
} else {
// stored 'dark', or no preference -> default to dark
document.documentElement.setAttribute('data-theme', 'dark');
}
} catch (e) {
document.documentElement.setAttribute('data-theme', 'dark');
}
})();
</script><!-- Umami analytics (self-hosted) — dashboard: https://stats.hoelee.com --><script defer src="https://stats.hoelee.com/script.js" data-website-id="1817d8f6-0e49-4b9a-a50d-307e4f2962c7" data-domains="blog.hoelee.com" type="b370bf50f4daa8c28be67203-text/javascript"></script><link rel="stylesheet" href="/_astro/about.Cf2K7Cwy.css"></head> <body> <header class="site-header"> <nav class="nav" aria-label="Main"> <a class="brand" href="/zh/">Mr<span class="dot"> Hoelee</span></a> <a href="/zh/posts/">文章</a> <a href="/zh/categories/">分类</a> <a href="/zh/about/">关于</a> <a href="/rss.xml" title="RSS">RSS</a> <button class="search-toggle" id="search-toggle" aria-label="搜索">
⌕ <span class="search-hint">搜索</span><kbd class="search-key">/</kbd> </button> <a class="lang-switch" href="/posts/running-ai-video-locally-on-a-12gb-gpu/" aria-label="Switch to English">EN</a> <button class="theme-toggle" id="theme-toggle" aria-label="Toggle theme">◐</button> </nav> </header> <main class="wrap"> <script type="application/ld+json">{"@context":"https://schema.org","@type":"BlogPosting","mainEntityOfPage":{"@type":"WebPage","@id":"https://blog.hoelee.com/posts/zh/running-ai-video-locally-on-a-12gb-gpu/"},"headline":"12GB 显卡跑本地 AI 视频:Wan 2.2 加 SeedVR2 的真实数据","description":"在 RTX 3060 上实测:Wan 2.2 TI2V-5B 生成、SeedVR2 放大、1080p 分辨率陷阱,以及那条让连续五次失败全部通过的命令。","image":"https://blog.hoelee.com/og/zh-running-ai-video-locally-on-a-12gb-gpu.jpg","inLanguage":"zh","datePublished":"2026-10-20T00:00:00.000Z","dateModified":"2026-10-20T00:00:00.000Z","author":{"@id":"https://blog.hoelee.com/#person"},"publisher":{"@id":"https://blog.hoelee.com/#person"},"isPartOf":{"@type":"Blog","@id":"https://blog.hoelee.com/#blog","name":"Mr Hoelee","url":"https://blog.hoelee.com"},"articleSection":"ai","keywords":"ai-video, wan, comfyui, seedvr2, local-llm, gpu, cost","wordCount":2448}</script> <article> <header class="article-head"> <h1>12GB 显卡跑本地 AI 视频:Wan 2.2 加 SeedVR2 的真实数据</h1> <div class="meta"> <time datetime="2026-10-20T00:00:00.000Z">2026年10月20日</time> <span class="sep">·</span> <span>8 分钟</span> <span class="sep">·</span> <a class="tag" href="/zh/categories/ai/">ai</a> <span class="tag">ai-video</span><span class="tag">wan</span><span class="tag">comfyui</span><span class="tag">seedvr2</span><span class="tag">local-llm</span><span class="tag">gpu</span><span class="tag">cost</span> </div> </header> <figure class="article-banner"> <picture> <source srcset="/banners/running-ai-video-locally-on-a-12gb-gpu.webp" type="image/webp"> <img src="/banners/running-ai-video-locally-on-a-12gb-gpu.jpg" alt="" loading="lazy" decoding="async" width="1600" height="900"> </picture> </figure> <p>所有讲本地 AI 视频的文章对 12GB 显卡的说法都差不多:小模型能跑,勉强。没有一篇说清楚”勉强”折算成多少分钟,也没人说产出到底是不是真的 1080p,更没人说最先崩的是哪一环。</p>
<p>我用一个晚上把这些问题问清楚了。硬件是 RTX 3060 12GB(i9-13900K、48GB 内存,ComfyUI 0.41.2 跑在 Windows 虚拟机里,显卡直通)。最后拿到一条流水线:一个提示词加一张起始图,就能产出真正的 1920×1080 镜头,不用按秒付钱给任何人。但它是在连续五次失败、而且崩在四个不同位置之后才跑通的。</p>
<p>这篇就是那晚的记录。</p>
<h2 id="我要的东西以及它和什么在竞争">我要的东西,以及它和什么在竞争</h2>
<p>目标很具体:每个镜头 5 秒,拼成更长的片子,分辨率 1080p。商业方案并不稀奇——Kling 的参考图生视频大约 $0.112/秒,5 秒镜头约 $0.56。如果一次就过,这个价格很便宜。</p>
<p>问题在于镜头不会一次就过。生成视频是个反复试的过程:改提示词、换起始帧、换种子、再跑一次。真正花钱的不是最后那一条,是教会我”最后一条该长什么样”的那三十条。这正好是本地显卡擅长的事。</p>
<h2 id="12gb-能装下的模型">12GB 能装下的模型</h2>
<table><thead><tr><th>文件</th><th>作用</th><th>大小</th></tr></thead><tbody><tr><td><code>wan2.2_ti2v_5B_fp16.safetensors</code></td><td>视频模型</td><td>9.99 GB</td></tr><tr><td><code>umt5_xxl_fp8_e4m3fn_scaled.safetensors</code></td><td>文本编码器</td><td>6.74 GB</td></tr><tr><td><code>wan2.2_vae.safetensors</code></td><td>视频 VAE</td><td>1.41 GB</td></tr><tr><td><code>seedvr2_3b_int8_convrot.safetensors</code></td><td>放大器</td><td>3.46 GB</td></tr><tr><td><code>seedvr2_ema_vae_fp16.safetensors</code></td><td>放大器 VAE</td><td>0.50 GB</td></tr></tbody></table>
<p>能用的是 Wan 2.2 TI2V-5B。它围绕 1280×704 训练,同一套权重同时做文生视频和图生视频,Apache-2.0 意味着产出归我。14B 版本更好,但装不下;LTX-2.3 是 22B,能跑,但那个速度会让”反复试”变成一种折磨。</p>
<h2 id="时间成本全部实测不是估算">时间成本(全部实测,不是估算)</h2>
<p>5 秒片段,121 帧、24fps、20 步采样:</p>
<table><thead><tr><th>分辨率</th><th>总计</th><th>采样</th><th>解码</th><th>显存峰值</th></tr></thead><tbody><tr><td>832×480</td><td>6.1 分钟</td><td>4.8 分钟</td><td>1.3 分钟</td><td>10.3 GB</td></tr><tr><td>1280×704</td><td>17.9 分钟</td><td>15.1 分钟</td><td>2.9 分钟</td><td>12.1 GB</td></tr></tbody></table>
<p>像素数减半,时间不是减半,而是降到约三分之一。这是扩散 Transformer 典型的超线性关系,也正是 480p 这档草稿存在的理由。</p>
<h2 id="第一次失败解码把主机内存吃光">第一次失败:解码把主机内存吃光</h2>
<p>第一次 720p 采样很顺利,然后死在 VAE 解码:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="plaintext"><code><span class="line"><span>RuntimeError: DefaultCPUAllocator: not enough memory:</span></span>
<span class="line"><span>you tried to allocate 1308426240 bytes</span></span></code></pre>
<p>这不是显存错误。121 帧的 Wan VAE 解码需要一次性开出约 1.3GB 的 CPU 侧缓冲,而虚拟机 48GB 里当时只剩约 12GB 空闲。改用分块解码——tile 272、overlap 32、temporal 32、temporal overlap 8——解码显存从 11.9GB 降到 3.9GB,跑通了。</p>
<p>教训不是”一律用分块解码”。25 帧时普通解码只要约 0.27GB,更快也更简单。解码方式应该跟着帧数走,而不是条件反射地选分块。</p>
<h2 id="第二次失败放大器把整个服务打死">第二次失败:放大器把整个服务打死</h2>
<p>SeedVR2 3B 的 1.5× 输出确实不错,但在 121 帧上它稳定地把进程干掉:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="plaintext"><code><span class="line"><span>Windows fatal exception: access violation</span></span>
<span class="line"><span> comfy/ldm/seedvr/vae.py ... decode_tiled</span></span></code></pre>
<p>这不是可以 catch 的异常,是原生崩溃,顺手把 HTTP 服务一起带走,端口直接死掉。装上 <code>triton-windows</code> 消掉了更早的那个 <code>PatchTritonVAE requires triton</code> 导入错误,但对崩溃毫无帮助。121 帧复现三次,41 帧一次都没崩。</p>
<p>所以绕过方式是切:把片段切成不超过 41 帧的段,分别放大,再拼接。难看,但可靠;只要每刀都落在精确的帧边界上,接缝看不出来。</p>
<h2 id="第三次失败以及那条解决问题的命令">第三次失败,以及那条解决问题的命令</h2>
<p>在一条干净的 5 秒渲染之后两小时,同一个脚本开始失败。然后又失败,而且报的错不一样。五次下来:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="plaintext"><code><span class="line"><span>aimdo: xfer_file_read_at: GetOverlappedResult failed error=1450</span></span>
<span class="line"><span>HostBuffer.read_file_slice failed ← 采样阶段</span></span>
<span class="line"><span>hostbuf_file_reader_read failed ← 解码阶段</span></span>
<span class="line"><span>Windows fatal exception: access violation ← 模型加载阶段</span></span></code></pre>
<p>我做了任何人都会做的事:怪到刚改过的东西头上——分辨率。于是跑了一次同帧数的 704p 对照。它也失败。分辨率从来就不是问题。</p>
<p>真正的原因:ComfyUI 在作业结束后把 10GB 模型留在显存里常驻。空闲时显存占着 9.7GB。下一次提交装不下,加载器就退回”从磁盘流式读权重切片”的路径,而那条路径在这台机器上是坏的——error 1450 是 <code>ERROR_NO_SYSTEM_RESOURCES</code>,属于主机侧资源失败,不是采样失败。</p>
<p>一个请求就解决,而且应该放在每次跑之前:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#B392F0">curl</span><span style="color:#79B8FF"> -X</span><span style="color:#9ECBFF"> POST</span><span style="color:#9ECBFF"> http://127.0.0.1:8188/free</span><span style="color:#79B8FF"> \</span></span>
<span class="line"><span style="color:#79B8FF"> -H</span><span style="color:#9ECBFF"> "Content-Type: application/json"</span><span style="color:#79B8FF"> \</span></span>
<span class="line"><span style="color:#79B8FF"> -d</span><span style="color:#9ECBFF"> '{"unload_models": true, "free_memory": true}'</span></span></code></pre>
<p>显存从 9.7GB 掉到 1.1GB,那个已经失败三次的作业下一次就过了。</p>
<p>还有一个”修复”必须点名,因为多数教程会推荐它:加 <code>--disable-dynamic-vram</code> 启动。它把崩溃提前了——9.99GB 的模型塞不进文本编码器剩下的 8.45GB,加载器转去切内存映射,在 <code>torch.storage</code> 里直接崩。同一堵墙,更糟的一面。</p>
<h2 id="1080p-的陷阱720-不是-32-的倍数">1080p 的陷阱:720 不是 32 的倍数</h2>
<p><code>Wan22ImageToVideoLatent</code> 要求宽高都是 32 的倍数。720 不是——720 ÷ 32 = 22.5——所以 720 以下最近的合法高度是 704,而 1280×704 是 16:8.8,不是 16:9。乘 1.5 之后,诚实的产出是 <strong>1920×1056</strong>,这不是 1080p,后面某处一定会出现黑边或被拉伸。</p>
<p>三条路:</p>
<table><thead><tr><th>做法</th><th>结果</th><th>代价</th></tr></thead><tbody><tr><td>生成 1280×<strong>736</strong>(23×32),放大 1.5× → 1920×1104,裁 24px</td><td>精确 1920×1080</td><td>无黑边、无变形</td></tr><tr><td>保持 704,补边</td><td>1920×1080</td><td>上下各 12px 黑边</td></tr><tr><td>保持 704,放大 1.534 后裁宽</td><td>1920×1080</td><td>损失约 2% 画面宽度</td></tr></tbody></table>
<p>我选第一条。736 是合法的 32 倍数,比 704 更接近 16:9,而上下裁掉 24 行只是去掉模型本来就没用上的边缘。</p>
<p>不要把 1056 拉伸成 1080。人脸会横向宽 2.3%,一眼就看得出不对。</p>
<h2 id="完整的流水线">完整的流水线</h2>
<p>先用 480p 出草稿,判断动作和构图——480p 的判断不了清晰度,所以别在那档评估画质。通过的镜头再走:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#6A737D"># 每次跑之前先清显存</span></span>
<span class="line"><span style="color:#B392F0">curl</span><span style="color:#79B8FF"> -X</span><span style="color:#9ECBFF"> POST</span><span style="color:#9ECBFF"> http://127.0.0.1:8188/free</span><span style="color:#79B8FF"> -d</span><span style="color:#9ECBFF"> '{"unload_models":true,"free_memory":true}'</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D"># 1. 按每秒 25 帧在 1280x736 生成</span></span>
<span class="line"><span style="color:#B392F0">python</span><span style="color:#9ECBFF"> bench_wan.py</span><span style="color:#79B8FF"> --w</span><span style="color:#79B8FF"> 1280</span><span style="color:#79B8FF"> --h</span><span style="color:#79B8FF"> 736</span><span style="color:#79B8FF"> --frames</span><span style="color:#79B8FF"> 25</span><span style="color:#79B8FF"> --steps</span><span style="color:#79B8FF"> 20</span><span style="color:#79B8FF"> --tag</span><span style="color:#9ECBFF"> final_1s</span><span style="color:#79B8FF"> --cold</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D"># 2. 每段不超过 41 帧,分别放大到 1920x1104</span></span>
<span class="line"><span style="color:#B392F0">python</span><span style="color:#9ECBFF"> bench_upscale.py</span><span style="color:#79B8FF"> --file</span><span style="color:#9ECBFF"> segment.mp4</span><span style="color:#79B8FF"> --scale</span><span style="color:#79B8FF"> 1.5</span><span style="color:#79B8FF"> --tsize</span><span style="color:#79B8FF"> 21</span><span style="color:#79B8FF"> --chunk</span><span style="color:#79B8FF"> 21</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D"># 3. 裁成精确 1080p</span></span>
<span class="line"><span style="color:#B392F0">ffmpeg</span><span style="color:#79B8FF"> -i</span><span style="color:#9ECBFF"> upscaled.mp4</span><span style="color:#79B8FF"> -vf</span><span style="color:#9ECBFF"> "crop=1920:1080:0:12"</span><span style="color:#79B8FF"> -c:v</span><span style="color:#9ECBFF"> libx264</span><span style="color:#79B8FF"> -crf</span><span style="color:#79B8FF"> 16</span><span style="color:#9ECBFF"> final.mp4</span></span></code></pre>
<p>下面这条就是在本地生成的输出,放在我自己的 CDN 上:1 秒、1920×1080、24fps,只给了一张起始图。</p>
<video controls playsinline preload="metadata" poster="https://content.hoelee.com/file/hoelee/video/Wan22-1080p-poster.jpg">
<source src="https://content.hoelee.com/file/h_1080/hoelee/video/Wan22-TI2V-5B-1080p-test.mp4" type="video/mp4">
你的浏览器不支持 HTML5 视频。
</video>
<p><em>Wan 2.2 TI2V-5B,1280×736 → SeedVR2 → 裁到 1920×1080。采样 2 分 13 秒,放大 2 分 49 秒,没有一帧是别人渲染的。</em></p>
<h2 id="放大到底买到了什么">放大到底买到了什么</h2>
<p>用拉普拉斯方差量,放大把细节能量从 673 提到 766,大约 14%。边缘、头发、霓虹招牌在 1:1 下确实更锐。</p>
<p>皮肤没有。3B 的放大器只是把画面里已有的信息锐化,它不会凭空造出毛孔或布料颗粒。把 SeedVR2 当成”修复”会失望;把它当成”让 720p 级的渲染在 1080p 下站得住”,才准确。</p>
<h2 id="结果">结果</h2>
<table><thead><tr><th>镜头长度</th><th>生成</th><th>放大</th><th>合计</th><th>成本</th></tr></thead><tbody><tr><td>1 秒(25 帧)</td><td>3.4 分钟</td><td>2.8 分钟</td><td>约 6 分钟</td><td>$0</td></tr><tr><td>5 秒(121 帧)</td><td>17.9 分钟</td><td>14.1 分钟</td><td>约 32 分钟</td><td>$0</td></tr></tbody></table>
<p>8 个镜头、60 秒的片子,本地大约半天算力;走商业 API 大约 $5,40 分钟。两个数字都是真的,回答的是不同问题。本地机器负责那三十条没人付钱的试错,API 负责最后那一条要交付的。</p>
<h2 id="如果重来一次">如果重来一次</h2>
<p>三件事,按能省的时间排序。</p>
<p><strong>先量显存,再怀疑代码。</strong> 那五次失败的弯路,是因为我相信”这套配置两小时前还能跑”,于是开始改参数。中间跑一次 <code>nvidia-smi</code> 就会看见 9.7GB 常驻,当场结束排查。</p>
<p><strong>设计流水线之前先读尺寸约束。</strong> 1920×1056 那个意外,来自没先确认 latent 节点要求 32 的倍数。这是五秒钟的检查,代价是一整轮重渲。</p>
<p><strong>一开始就把放大切成段。</strong> 41 帧这个上限听起来很随意,直到第一次崩溃为止。现在它就是工作单位。</p>
<p>诚实的总结:12GB 足够在本地做 1080p AI 视频,大约每完成 1 秒成片要 6 分钟;但它不足以让人对内存掉以轻心。</p>
<hr>
<p><em>如果你想把这类东西搬进自己的生意——本地视频生成、自托管的 AI 流水线,或者让一张显卡持续干活又不用工程师盯着的那套管道——这些我都做:</em></p>
<ul>
<li>📱 <strong>WhatsApp:</strong> <a href="https://wa.me/60127972969">+60 12-797 2969</a></li>
<li>📧 <strong>邮件:</strong> <a href="/cdn-cgi/l/email-protection#274a42674f48424b42420944484a185452454d4244531a026211021e64026664026212021e64026517021517666e02151702621f026610021f1102621e026615021e16026211026512021f16026211026517026513026210026566026561"><span class="__cf_email__" data-cfemail="69040c2901060c050c0c470a0604">[email&#160;protected]</span></a></li>
<li>🌐 <strong>网站:</strong> <a href="https://hoelee.com">hoelee.com</a></li>
</ul> <div class="author-card"> <picture> <source srcset="/images/lee-teong-hoe-128.webp" type="image/webp"> <img class="avatar" src="/images/lee-teong-hoe-128.jpg" alt="Lee Teong Hoe" width="64" height="64" loading="lazy" decoding="async"> </picture> <div class="author-body"> <p class="who">Lee Teong Hoe · Mr Hoelee</p> <p class="bio">Full-stack developer &amp; DevOps engineer based in Malaysia. I run my own production infrastructure and build web systems for small businesses — this blog is the living record.</p> <div class="links"> <a href="https://www.linkedin.com/in/hoelee" target="_blank" rel="noopener">LinkedIn</a> <a href="https://github.com/hoelee" target="_blank" rel="noopener">GitHub</a> <a href="https://git.hoelee.com/hoelee" target="_blank" rel="noopener">Gitea</a> <a href="/cdn-cgi/l/email-protection#751810351d1a101910105b161a18"><span class="__cf_email__" data-cfemail="640901240c0b010801014a070b09">[email&#160;protected]</span></a> </div> <p class="author-split"> <a href="/about/">Hiring? Profile &amp; stack</a> <span class="sep">·</span> <a href="https://www.hoelee.com" target="_blank" rel="noopener">Malaysian SME? Website design &amp; development ↗</a> <span class="sep">·</span> <a href="/posts/">All posts</a> </p> </div> </div> </article> <section class="related"> <h2>相关文章</h2> <div class="related-grid"> <a class="related-card" href="/posts/zh/why-i-still-bought-a-local-gpu/"> <div class="related-body"> <span class="related-badge">ai</span> <h3>Claude 和 GPT 明明更強,我為什麼還是買了 GPU 跑本地 AI</h3> <time datetime="2026-09-21T00:00:00.000Z"> 2026年9月21日 · 9 分钟 </time> </div> </a><a class="related-card" href="/posts/zh/best-ai-video-generators-2026/"> <div class="related-body"> <span class="related-badge">ai</span> <h3>2026 年最佳 AI 视频生成器:免费与付费对比,以及我的 LTX-2.3 实测</h3> <time datetime="2026-09-11T00:00:00.000Z"> 2026年9月11日 · 19 分钟 </time> </div> </a><a class="related-card" href="/posts/zh/five-step-ai-roadmap-what-i-actually-run/"> <div class="related-body"> <span class="related-badge">ai</span> <h3>全网都在转的 AI 五步学习路线 —— 我真正在生产里跑的是哪几步</h3> <time datetime="2026-10-13T00:00:00.000Z"> 2026年10月13日 · 10 分钟 </time> </div> </a><a class="related-card" href="/posts/zh/i-let-an-agent-manage-my-shopee-vouchers/"> <div class="related-body"> <span class="related-badge">ai</span> <h3>我让 AI 助手管我的 Shopee 优惠券 —— 它建了一张我没批准的券</h3> <time datetime="2025-07-08T00:00:00.000Z"> 2025年7月8日 · 7 分钟 </time> </div> </a> </div> </section> </main> <footer class="site-footer"> <span>© 2026 Lee Teong Hoe · Mr Hoelee</span> <span> <a href="https://www.linkedin.com/in/hoelee" target="_blank" rel="noopener">LinkedIn</a> <a href="https://github.com/hoelee" target="_blank" rel="noopener">GitHub</a> <a href="https://git.hoelee.com/hoelee" target="_blank" rel="noopener">Gitea</a> <a href="https://www.hoelee.com" target="_blank" rel="noopener">hoelee.com</a> </span> </footer> <div class="search-overlay" id="search-overlay" hidden data-astro-cid-otpdt6jm> <div class="search-panel" data-astro-cid-otpdt6jm> <div class="search-bar" data-astro-cid-otpdt6jm> <input type="search" id="search-input" placeholder="搜索文章…" autocomplete="off" spellcheck="false" aria-label="搜索文章…" data-astro-cid-otpdt6jm> <button class="search-close" id="search-close" aria-label="Close" data-astro-cid-otpdt6jm>✕</button> </div> <div class="search-results" id="search-results" data-astro-cid-otpdt6jm></div> </div> <!-- Dead-end screen for a search with no hits: real links, rendered at build time. --> <template id="search-empty-tpl" data-astro-cid-otpdt6jm> <p class="excerpt" data-role="message" data-astro-cid-otpdt6jm>没有匹配的结果。</p> <p class="excerpt" data-role="try" data-astro-cid-otpdt6jm>试试这些主题:</p> <p class="search-empty-chips" data-astro-cid-otpdt6jm> <a class="chip" href="/zh/tags/docker/" data-astro-cid-otpdt6jm>docker</a><a class="chip" href="/zh/tags/self-hosting/" data-astro-cid-otpdt6jm>self-hosting</a><a class="chip" href="/zh/tags/nginx/" data-astro-cid-otpdt6jm>nginx</a><a class="chip" href="/zh/tags/monitoring/" data-astro-cid-otpdt6jm>monitoring</a><a class="chip" href="/zh/tags/automation/" data-astro-cid-otpdt6jm>automation</a> <a class="chip" href="/zh/posts/" data-astro-cid-otpdt6jm>浏览全部文章</a> </p> </template><a class="chip" href="/zh/posts/" data-astro-cid-otpdt6jm></a></div><a class="chip" href="/zh/posts/" data-astro-cid-otpdt6jm> <script data-cfasync="false" src="/cdn-cgi/scripts/5c5dd728/cloudflare-static/email-decode.min.js"></script><script type="b370bf50f4daa8c28be67203-module">const n=document.getElementById("search-overlay"),l=document.getElementById("search-input"),t=document.getElementById("search-results"),E=document.getElementById("search-close");let i;function y(){n.hidden=!1,l.value="",t.innerHTML="",l.focus()}function o(){n.hidden=!0}const f=document.getElementById("search-toggle");f&&f.addEventListener("click",y);E.addEventListener("click",o);n.addEventListener("click",e=>{e.target===n&&o()});document.addEventListener("keydown",e=>{e.key==="Escape"&&!n.hidden&&o(),(e.ctrlKey||e.metaKey)&&e.key==="k"&&(e.preventDefault(),n.hidden?y():o())});async function g(){return i||(i=await import("/pagefind/pagefind.js"),i)}let m;l.addEventListener("input",()=>{const e=l.value.trim();if(!e){t.innerHTML="";return}m&&clearTimeout(m),m=setTimeout(async()=>{try{const r=await g(),p=await(r.debouncedSearch?r.debouncedSearch(e):r.search(e));if(t.innerHTML="",!p.results.length){t.innerHTML="";const s=document.getElementById("search-empty-tpl");s&&t.appendChild(s.content.cloneNode(!0));return}for(const s of p.results.slice(0,12)){const d=await s.data(),c=document.createElement("a");c.className="search-result",c.href=d.url;const h=document.createElement("h3");h.textContent=d.meta.title||d.url;const u=document.createElement("p");u.className="excerpt",u.innerHTML=d.excerpt||"",c.appendChild(h),c.appendChild(u),t.appendChild(c)}}catch{t.innerHTML="";const a=document.createElement("p");a.className="excerpt",a.textContent="Search is unavailable.",t.appendChild(a)}},120)});</script></a> <script type="b370bf50f4daa8c28be67203-module">const o=document.getElementById("theme-toggle");o&&o.addEventListener("click",()=>{const e=document.documentElement.getAttribute("data-theme")==="dark"?"light":"dark";document.documentElement.setAttribute("data-theme",e);try{localStorage.setItem("theme",e)}catch{}});document.querySelectorAll("pre").forEach(t=>{const e=document.createElement("div");e.className="code-block",t.parentNode.insertBefore(e,t),e.appendChild(t);const n=document.createElement("button");if(n.className="copy-btn",n.textContent="copy",n.addEventListener("click",()=>{const c=t.innerText;navigator.clipboard.writeText(c).then(()=>{n.textContent="copied",n.classList.add("copied"),setTimeout(()=>{n.textContent="copy",n.classList.remove("copied")},1500)})}),e.appendChild(n),t.scrollHeight>420){e.classList.add("is-collapsed");const c=document.createElement("button");c.className="code-expand",c.textContent=`Expand (${Math.round(t.scrollHeight/22)} lines) ↓`,c.addEventListener("click",()=>{e.classList.remove("is-collapsed"),c.remove()}),e.appendChild(c)}});document.addEventListener("keydown",t=>{const e=t.target;t.key==="/"&&!t.ctrlKey&&!t.metaKey&&!(e&&(e.tagName==="INPUT"||e.tagName==="TEXTAREA"||e.isContentEditable))&&(t.preventDefault(),document.getElementById("search-toggle")?.click())});</script> <script src="/cdn-cgi/scripts/7d0fa10a/cloudflare-static/rocket-loader.min.js" data-cf-settings="b370bf50f4daa8c28be67203-|49" defer></script></body> </html>