Select Page

使用 Claude Code 搭配 LM Studio 與 Ollama:打造零 API 成本 AI 開發環境

Claude Code 最大特色之一,就是它能直接理解整個專案目錄、修改檔案、執行 CLI 指令,甚至自動修復程式碼問題。

但許多人最在意的是:

  • API 費用太高
  • 原始碼不想送雲端
  • 想完全離線使用
  • 希望使用自己的 Local LLM

現在透過 Ollama 官方網站LM Studio 官方網站,已經可以讓 Claude Code 直接使用本地模型。

本篇文章會完整介紹:

  • Claude Code 是什麼
  • 如何讓 Claude Code 使用 Local LLM
  • Ollama 與 LM Studio 差異
  • 三種實作方式
  • Web Search 功能啟用
  • 常用 CLI 指令
  • 適合的模型推薦

什麼是 Claude Code?

Claude 官方網站 的 Claude Code 是 Anthropic 推出的 AI Coding Agent。

它並不是單純聊天工具,而是:

  • 能讀取整個專案
  • 可修改程式碼
  • 可執行 Terminal 指令
  • 可自動修 Bug
  • 可跨多檔案操作
  • 支援 Agent Workflow

官方描述 Claude Code 是一個:

AI-powered coding assistant that helps you build features, fix bugs, and automate development tasks.


為什麼大家開始用 Local LLM?

Local LLM 的優勢非常明顯:

功能雲端模型Local LLM
隱私程式碼送雲端完全本地
費用API Token 收費幾乎免費
離線不可
速度看網路本機 GPU
自訂模型有限制完全自由

尤其現在 Ollama 已支援 Anthropic Messages API,相容 Claude Code。


方法一:Claude + VSCode + Ollama / LM Studio

這是目前最多人使用的方法。

架構圖

Claude Code     ↓VSCode Extension     ↓Ollama / LM Studio     ↓Local LLM

安裝流程

Step 1:安裝 Claude Code

官方下載:

Claude Download 官方下載頁面

Linux / macOS:

curl -fsSL https://claude.ai/install.sh | bash

Step 2:安裝 Ollama

官方網站:

Ollama 官方網站

Linux:

curl -fsSL https://ollama.com/install.sh | sh

Step 3:下載模型

推薦模型:

ollama pull qwen3-coder

或:

ollama pull deepseek-coder-v2

Step 4:啟動模型

ollama run qwen3-coder

LM Studio 使用方式

如果你不喜歡 CLI,可以使用 LM Studio。

LM Studio 官方網站

LM Studio 特點:

  • GUI 操作
  • 支援 OpenAI API
  • 支援本地 Server
  • 支援 GPU Offload
  • Windows 體驗很好

有些使用者甚至認為 LM Studio 在 Windows + iGPU 上比 Ollama 更方便。


Claude Code 連接 Ollama

設定環境變數:

export ANTHROPIC_BASE_URL=http://localhost:11434
export ANTHROPIC_AUTH_TOKEN=your_token
export CLAUDE_CODE_EFFORT_LEVEL=low

執行:

claude

Claude Code 即會透過 Ollama 使用本地模型。


方法二:使用 ollama launch claude

這是 Ollama 官方提供的整合方式。

官方文件:

Ollama Claude Code Integration 文件


安裝方式

更新 Ollama:

ollama update

執行:

ollama launch claude

這會:

  • 自動設定 Claude Code
  • 自動串接 Anthropic-compatible API
  • 使用本地模型

官方支援模型

目前官方文件中提到可搭配:

  • qwen3.5
  • glm-5
  • kimi-k2.5

等模型。


方法三:使用 free-claude-code Gateway

GitHub:

free-claude-code GitHub 專案

這個專案本質上是一個:

Claude Code Gateway Proxy

它能:

  • 將 Claude Code API 轉向 Local LLM
  • 模擬 Anthropic API
  • 轉接 Ollama / OpenAI API
  • 避免官方限制

適合使用情境

非常適合:

  • 本地 AI 開發環境
  • 多模型切換
  • 企業內網
  • 私有化部署
  • AI Coding Lab

啟動方式

通常為:

git clone https://github.com/Alishahryar1/free-claude-codecd free-claude-codenpm installnpm start

再讓 Claude Code 指向 Gateway。


啟用 Ollama Web Search 功能

Ollama 現在已支援 Web Search。

官方文件:

Ollama Web Search 文件


Web Search 功能用途

可以讓 Local LLM:

  • 搜尋最新資訊
  • 查 Stack Overflow
  • 查 GitHub
  • 查文件
  • 即時查詢

這對 Claude Code 非常重要。

因為 Coding Agent 若沒有 Web Search:

  • 容易使用舊知識
  • 不知道最新版套件
  • 不知道最新 API

啟用方式

通常:

OLLAMA_WEB_SEARCH=true

或:

export OLLAMA_WEB_SEARCH=true

依照官方文件設定即可。


推薦 Local LLM 模型

程式開發最佳選擇

模型推薦度特點
Qwen3-Coder★★★★★Coding 能力極強
DeepSeek Coder V2★★★★★開源熱門
GLM-5★★★★☆中文能力佳
Kimi K2.5★★★★☆長上下文
Gemma 3★★★☆☆輕量快速

Claude Code 常用指令

啟動 Claude Code

claude

指定 API

ANTHROPIC_BASE_URL=http://localhost:11434 claude

指定模型

ANTHROPIC_MODEL=qwen3-coder claude

查看 Ollama 模型

ollama list

啟動 Ollama Server

ollama serve

Ollama vs LM Studio 比較

功能OllamaLM Studio
CLI普通
GUI基本非常完整
Windows普通非常好
API
Docker普通
GPU 管理CLIGUI
新手友善中等

Claude Code + Local LLM 的實際優勢

1. 幾乎零成本

不再需要:

  • Anthropic API
  • OpenAI API
  • Token 費用

2. 完全私有化

原始碼不離開本機。

非常適合:

  • 企業
  • 資安環境
  • NDA 專案
  • 內網系統

3. 多模型自由切換

你可以:

  • 今天用 Qwen
  • 明天用 DeepSeek
  • 後天用 Kimi

不受平台限制。


我的實際建議

如果你是:

新手

建議:

LM Studio + Claude Code

因為 GUI 最簡單。


Linux / DevOps / AI 工程師

建議:

Ollama + Claude Code

CLI 整合能力非常強。


企業環境

建議:

free-claude-code Gateway + Ollama

可做到:

  • API Gateway
  • 多模型管理
  • 權限控管
  • 私有化部署

結論

Claude Code 正在快速成為下一代 AI Coding Agent。

而現在最重要的變化是:

Claude Code 已經不再只能綁定官方 Claude API。

透過:

  • Ollama
  • LM Studio
  • free-claude-code
  • Anthropic-compatible API

你已經可以:

  • 完全本地化
  • 零 API 成本
  • 自由切換模型
  • 保護原始碼隱私

對於 AI 開發者與工程團隊來說,這將是非常重要的開發趨勢。


下載資源

官方網站


參考資料

Ollama + Qwen 3.6 怎麼選?27B、35B、MXFP8、NVFP4 完整比較與推薦

最新的 Qwen 3.6,在 Ollama 上的表現,可以說是目前「本地 Coding 模型」中非常強勢的一個系列。

如果你正在使用:

  • NVIDIA Spark
  • RTX 顯卡
  • Ollama
  • OpenWebUI
  • Continue
  • Claude Code
  • OpenHands
  • Hermes Agent
  • Cursor 類工具
  • Apple

那麼 Qwen 3.6 幾乎一定值得研究。

這篇文章會完整解析:

  • Qwen 3.6 每個版本差異
  • 27B 與 35B 的差異
  • MXFP8、NVFP4、BF16 是什麼
  • 哪個最適合寫程式
  • NVIDIA Spark 最推薦的配置
  • Ollama 部署建議
  • 多人 SaaS / AI Agent 最佳實務

什麼是 Qwen 3.6?

Qwen 是阿里巴巴推出的大型語言模型(LLM)系列。

最新的 Qwen 3.6,官方特別強調:

  • Agentic Coding
  • Repository-level Reasoning
  • 長 Context 推理
  • Thinking Preservation

也就是說:

它不只是會寫程式,而是開始能理解「整個專案」。

根據官方與 Ollama 頁面資訊,Qwen 3.6 在以下方面有明顯提升:

  • 前端工作流理解
  • 多檔案推理
  • AI Agent Tool Calling
  • 長上下文理解
  • 歷史推理保留
  • Repository 級別程式分析

為什麼 Qwen 3.6 很適合 Ollama?

Qwen 3.6 最大特色之一:

就是對本地部署非常友善。

目前 Ollama 已提供大量版本:

  • 27B
  • 35B-A3B
  • Coding 版本
  • Vision 版本
  • MXFP8
  • NVFP4
  • BF16
  • MLX

而且幾乎都支援:

  • 256K Context
  • 長文本推理
  • 本地 AI Agent
  • Coding Workflow

Qwen 3.6 各版本意思解析

qwen3.6:latest

這是官方最新預設版本。

特色:

  • 通用型
  • 支援圖片
  • 適合聊天與分析
  • 多模態能力

適合:

  • OpenWebUI
  • AI 助理
  • OCR
  • 圖片分析

但:

不是最強的 Coding 版本。


qwen3.6:27b

27B = 270億參數。

這是目前非常熱門的甜蜜點。

優點:

  • Coding 能力很強
  • 推理速度快
  • VRAM 壓力較低
  • 多人共享容易

非常適合:

  • Continue
  • Claude Code
  • VSCode AI
  • Agent Workflow
  • 本地 Copilot

qwen3.6:35b

35B = 350億參數。

這類模型:

推理能力更強。

尤其在:

  • 大型專案理解
  • 架構設計
  • Refactor
  • 多檔案分析

會比 27B 更好。

但缺點:

  • 更吃 VRAM
  • 速度較慢
  • 成本較高

什麼是 Coding 版本?

例如:

  • qwen3.6:27b-coding-mxfp8
  • qwen3.6:35b-a3b-coding-nvfp4

這些是:

專門針對寫程式優化的模型。

相較一般聊天模型:

它們更擅長:

  • Python
  • TypeScript
  • Go
  • Rust
  • Docker
  • Shell
  • Kubernetes
  • Debug
  • Refactor
  • AI Agent Tool Calling

官方也特別提到:

Qwen 3.6 在 Agentic Coding 與 Repository-level reasoning 上有大幅提升。


MXFP8、NVFP4、BF16 是什麼?

很多人看到:

  • MXFP8
  • NVFP4
  • BF16

會很混亂。

其實這些都是:

「量化格式」。


MXFP8

例如:

qwen3.6:27b-coding-mxfp8

這是 NVIDIA 新世代 FP8 格式。

特色:

  • 品質高
  • VRAM 使用合理
  • 推理速度快
  • 非常適合 NVIDIA GPU

目前很多人認為:

MXFP8 是本地 AI Coding 的最佳甜蜜點。

尤其適合:

  • NVIDIA Spark
  • RTX 4090
  • RTX 5090
  • 多 Agent Workflow

NVFP4

例如:

qwen3.6:27b-coding-nvfp4

這是 NVIDIA 的 4-bit 浮點量化格式。

特色:

  • 更省 VRAM
  • 更快
  • 可多人共享
  • 吞吐量高

但:

推理品質會稍微下降。

比較適合:

  • SaaS 平台
  • 多人 AI IDE
  • 高併發 Agent

目前學術研究也開始針對 NVFP4 做最佳化。


BF16

例如:

qwen3.6:27b-coding-bf16

這幾乎是:

接近原始精度。

優點:

  • 品質最高
  • reasoning 最穩
  • hallucination 較少

缺點:

  • 超級吃 VRAM
  • 非常耗記憶體
  • 多人共享困難

適合:

  • 單人高品質開發
  • 研究用途
  • 極限推理

MLX 是什麼?

MLX 是 Apple Silicon 專用。

例如:

  • M1
  • M2
  • M3
  • M4

什麼是 A3B?

例如:

qwen3.6:35b-a3b-coding-mxfp8

這代表:

MoE(Mixture of Experts)架構。

意思是:

模型總參數很大,但每次只啟用部分專家。

優點:

  • 更聰明
  • 更快
  • 成本更低
  • 推理效率高

官方指出:

Qwen3.6-35B-A3B 僅啟動約 3B Active Parameters,但依然能超越部分大型 Dense 模型。


NVIDIA Spark 最推薦哪個?

如果你的環境是:

  • NVIDIA Spark
  • CUDA 13
  • 128GB RAM
  • Ollama
  • OpenWebUI
  • Continue
  • Claude Code
  • OpenHands

那我目前最推薦:


🥇 最推薦:qwen3.6:27b-coding-mxfp8

推薦原因:

  • Coding 非常強
  • 推理速度快
  • VRAM 不容易爆
  • Agent 很穩
  • 長 Context 表現好
  • 本地部署平衡最佳

這是目前真正的:

「Production Sweet Spot」。


🥈 高階推理推薦:qwen3.6:35b-a3b-coding-mxfp8

適合:

  • AI Agent
  • 大型專案
  • 架構設計
  • 多 Repo 分析

優點:

  • reasoning 更強
  • repository 理解更強
  • 複雜任務更穩

缺點:

  • 比較慢
  • VRAM 需求更高

🥉 多人 SaaS 推薦:qwen3.6:27b-coding-nvfp4

適合:

  • 多人共享
  • SaaS
  • AI IDE
  • 高併發 Agent

優點:

  • 非常省 VRAM
  • 吞吐量高
  • 可同時服務多人

但:

品質會略低於 MXFP8。


我自己的實戰看法

如果你是:

「真正要拿來工作」。

我目前認為:

Qwen 3.6 已經開始接近:

「本地版 Claude Code」。

尤其:

27B Coding MXFP8。

真的已經非常強。

它最大的優勢不是單純寫程式。

而是:

  • 能理解整個 Repo
  • 能做 Agent 工作流
  • 能做長 Context reasoning
  • 能做 Tool Calling
  • 能理解大型專案

這跟以前單純「補程式碼」的模型完全不同。


Ollama 部署建議

安裝模型

ollama pull qwen3.6:27b-coding-mxfp8

執行模型

ollama run qwen3.6:27b-coding-mxfp8

開放 API

export OLLAMA_HOST=0.0.0.0:11434

NVIDIA Spark 最佳化建議

建議環境變數:

Environment="OLLAMA_HOST=0.0.0.0:11434"
Environment="OLLAMA_NUM_PARALLEL=4"
Environment="OLLAMA_MAX_LOADED_MODELS=3"
Environment="OLLAMA_MAX_QUEUE=1024"
Environment="OLLAMA_KEEP_ALIVE=-1"
Environment="OLLAMA_FLASH_ATTENTION=1"
Environment="OLLAMA_KV_CACHE_TYPE=q8_0"
Environment="OMP_NUM_THREADS=32"

適合搭配的工具

Qwen 3.6 很適合:

  • Continue
  • Claude Code
  • OpenHands
  • Hermes Agent
  • OpenWebUI
  • Cursor 類工具
  • Browser-use
  • AI Agent Workflow

結論

如果你現在想打造:

  • 本地 AI Coding 環境
  • AI Agent 平台
  • 多人 AI IDE
  • 本地 Claude Code
  • Ollama SaaS

那麼:

Qwen 3.6 幾乎是目前最值得研究的一條路。

尤其:

qwen3.6:27b-coding-mxfp8

我認為:

這是目前 NVIDIA Spark 上:

最平衡、最實用、最值得長期使用的本地 Coding 模型之一。

參考資料

Nvidia Spark Ollama 的滿血設定

🧱 直接給「滿血設定」

你現在 service 改成這個👇(最重要)

[Unit]
Description=Ollama Service
After=network-online.target[Service]
ExecStart=/usr/local/bin/ollama serve
User=ollama
Group=ollama
Restart=always
RestartSec=3# ===== 核心 =====
Environment="OLLAMA_HOST=0.0.0.0:11434"
Environment="OLLAMA_KEEP_ALIVE=-1"# ===== GPU 強制 =====
Environment="OLLAMA_GPU_LAYERS=999"# ===== 記憶體優化 =====
Environment="OLLAMA_KV_CACHE_TYPE=q8_0"
Environment="OLLAMA_FLASH_ATTENTION=1"# ===== Spark專用 tuning =====
Environment="OLLAMA_NUM_PARALLEL=2"
Environment="OLLAMA_MAX_LOADED_MODELS=2"
Environment="OLLAMA_MAX_QUEUE=512"# ===== CPU 控制 =====
Environment="OMP_NUM_THREADS=20"# ===== PATH =====
Environment="PATH=/usr/local/cuda/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"[Install]
WantedBy=multi-user.target

🔁 套用

sudo systemctl daemon-reexec
sudo systemctl daemon-reload
sudo systemctl restart ollama

🔍 一定要做驗證

systemctl show ollama | grep OLLAMA

👉 要看到全部變數


🧠 模型才是性能關鍵

🔥 Spark 正確用法

用途模型
最快llama3:8b
平衡llama3:13b
大模型30B(單一)

✅ 先做這個測試(非常重要)

ollama run llama3

👉 再看:

nvidia-smi

🔥 讓 GPU 真的滿血(關鍵3件事)

① 模型「完全進 GPU」

👉 關鍵判斷:

ollama ps

看到:

100% GPU

👉 才算成功


② context 不要亂開

👉 Ollama 預設 4096

👉 你如果開到:

  • 32K / 128K

👉 = 直接 CPU fallback


👉 建議:

ollama run llama3 --num_ctx 4096

③ 不要多模型併發

Spark 特性:

👉 記憶體大,但 bandwidth 普通

👉 所以:

OLLAMA_NUM_PARALLEL=2

是最佳解


🧠 Spark 的本質

👉 DGX Spark

  • 128GB memory ✔
  • 超大模型可跑 ✔
  • ❌ 不是高吞吐 GPU

👉 正確定位:

🔥 大模型單機推理機


🧪 接下就會看到的改善

調完後:

指標改善
GPU Util0% → 80%+
token/sec↑ 3~10倍
latency↓ 50%以上
CPU

🎯 注意事項

👉 ❗不要讓使用者直接打 Ollama


Deprecated: explode(): Passing null to parameter #2 ($string) of type string is deprecated in C:\inetpub\raintips\wp-content\themes\Divi\core\functions.php on line 1613

Deprecated: explode(): Passing null to parameter #2 ($string) of type string is deprecated in C:\inetpub\raintips\wp-content\themes\Divi\core\functions.php on line 1613

Deprecated: explode(): Passing null to parameter #2 ($string) of type string is deprecated in C:\inetpub\raintips\wp-content\themes\Divi\core\functions.php on line 1613
Llama-3.2-90B-Vision 實戰:本地端影片偵探與多模態分析完整解析

Llama-3.2-90B-Vision 實戰:本地端影片偵探與多模態分析完整解析


Deprecated: explode(): Passing null to parameter #2 ($string) of type string is deprecated in C:\inetpub\raintips\wp-content\themes\Divi\core\functions.php on line 1613

Deprecated: explode(): Passing null to parameter #2 ($string) of type string is deprecated in C:\inetpub\raintips\wp-content\themes\Divi\core\functions.php on line 1613

Deprecated: explode(): Passing null to parameter #2 ($string) of type string is deprecated in C:\inetpub\raintips\wp-content\themes\Divi\core\functions.php on line 1613

前言:為什麼要做「影片偵探」?

隨著監控攝影機、行車紀錄器、門禁與智慧製造設備的大量部署,「影片」早已成為企業與政府最重要的資料來源之一。然而,影片資料無法被傳統文字搜尋系統理解,長時間的人工作業也帶來極高成本。

這正是「影片偵探(Video Detective)」的價值所在——
讓 AI 看懂影片內容、理解畫面語意、並能以自然語言回答問題

而在 2024 年 9 月 25 日,Meta 推出的 Llama-3.2-90B-Vision(Vision 3.2 90B),正式讓「本地端、多模態、可商業應用」的影片理解系統成為可能。


Llama-3.2-90B-Vision 模型簡介

Vision 3.2 90B 是 Meta 在 Llama 3.2 系列中最強大的開源多模態模型(VLM),也是目前開源社群中少數能與封閉模型正面競爭的選擇。

核心規格與特點

  • 模型規模
    900 億個參數(90B),為 Llama 3.2 系列最大版本
  • 視覺處理能力
    • 圖像理解
    • 圖像描述(Image Captioning)
    • 視覺問答(VQA)
    • 圖表與截圖解析(Excel / PDF / 監控畫面)
  • 技術架構
    採用 Adapter Weights,將視覺編碼器整合至 Llama 3 語言模型
  • 上下文長度
    高達 128K tokens,可處理長影片摘要與多畫面分析
  • 效能表現
    在多模態基準測試中,接近 GPT-4o-mini、Claude 3 Haiku
  • 硬體需求
    通常需要多張 NVIDIA A100 / H100 GPU 進行推理

官方資源:


什麼是「影片偵探」?

「影片偵探」並不是單一模型,而是一套 多階段 AI 架構

  1. 影格抽取(Frame Sampling)
  2. 物件 / 人臉偵測(YOLO / yolo face)
  3. 視覺語意理解(Llama-3.2-90B-Vision)
  4. 語意摘要與事件推論
  5. 自然語言查詢與回應

使用者可以直接詢問:

「這段影片中,有沒有可疑人物?」
「哪一段出現未授權進入?」
「請總結這 2 小時監控畫面的重點事件」


為何選擇 Llama-3.2-90B-Vision?

1️⃣ 本地端部署(On-Premise)

對於以下場景尤其重要:

  • 政府監控系統
  • 金融機構
  • 工廠產線
  • 企業內部資安影像

資料不出內網、不上雲端,符合資安與法規要求。

透過 Ollama 即可在本地部署 Vision 3.2:

👉 https://ollama.com/library/llama3.2-vision:90b

參考教學:


2️⃣ 高度客製化的影片理解流程

你可以自由設計:

  • 先用 YOLO / yolo face 做人臉與物件偵測
  • 再將關鍵影格丟給 Llama-3.2-90B-Vision 理解語意
  • 根據產業需求自訂 Prompt 與規則

例如:

  • 工地安全(是否未戴安全帽)
  • 店面防盜(可疑行為模式)
  • 智慧製造(異常操作流程)

3️⃣ 可合法商業應用的開源模型

與多數封閉 API 不同,Llama 3.2 系列具備商業使用條款,適合:

  • SaaS 影片分析平台
  • 企業內部 AI 系統
  • 客製化專案與整合案

同時避免:

  • API 呼叫成本失控
  • 雲端延遲
  • 資料外洩風險

架構範例:影片偵探系統設計

線上體驗與 API 使用方式

如果暫時沒有高階 GPU,也可以使用 NVIDIA 官方提供的線上版本:


實作系統

影片偵探 Prompt 範例(可直接用)

Prompt 1:單張關鍵影格「事件描述 + 可疑度」

用途:把 YOLO 的偵測結果 + 影格一起丟給 Vision 3.2,產出可索引的敘述。
(強烈建議把 YOLO metadata 一起餵,讓 VLM 不用「瞎猜」類別與位置)

你是一個「影片偵探」AI,負責為監控影格做事件描述與風險判讀。
請根據:
1) 影格畫面內容
2) 我提供的偵測結果(物件類別、bbox、信心值)
輸出結構化 JSON,方便後續索引與查詢。

規則:
- 不要編造畫面中不存在的事物。
- 若畫面資訊不足,請在 unsure 字段說明原因。
- suspicious_score 0~100:越高越可疑,但必須用畫面線索與偵測結果解釋。
- 需同時輸出一段適合全文檢索的繁體中文摘要 summary_zh。

輸入:
timestamp: {{timestamp}}
camera_id: {{camera_id}}
detections: {{yolo_detections_json}}
image: <提供影像>

輸出 JSON 欄位:
{
  "timestamp": "...",
  "camera_id": "...",
  "scene_summary": "...",
  "entities": [
    {"type": "...", "count": 1, "notable": "..."}
  ],
  "actions": ["..."],
  "suspicious_score": 0,
  "suspicious_reasons": ["..."],
  "unsure": ["..."],
  "summary_zh": "..."
}

Prompt 2:多張影格「跨時間軸事件摘要」

用途:把同一事件的多張關鍵影格(例如 5~12 張)一次丟給 128K context,做「時間序列推論」

你是一個監控「影片偵探」AI。以下提供同一段時間序列的多張關鍵影格,
每張都有 timestamp 與偵測結果。請完成:

1) 以時間順序描述發生了什麼事(避免臆測)。
2) 是否存在可疑行為?若有,指出關鍵時間點與依據。
3) 產出適合儲存到事件資料庫的 JSON(含 event_type、start/end、關聯track_id)。

輸入格式(重複多次):
- frame_i.timestamp: ...
- frame_i.detections: ...
- frame_i.image: <圖>

輸出:
- timeline_bullets(條列,含時間)
- event_json:
{
  "event_type": "...",
  "start_time": "...",
  "end_time": "...",
  "actors": [{"track_id":"...","type":"person/vehicle","notes":"..."}],
  "evidence": [{"timestamp":"...","what":"..."}],
  "risk_level": "low/medium/high",
  "search_keywords_zh": ["..."]
}

Prompt 3:使用者查詢「找片段 + 佐證」

用途:RAG 從索引庫找出候選事件後,叫模型做最後整合回答。

你是影片偵探助理。你會收到:
- 使用者問題
- 檢索得到的候選事件清單(含摘要、時間、相機、佐證影格描述)

請:
1) 直接回答問題(繁體中文)
2) 附上最相關的事件時間範圍(start~end)
3) 列出 1~3 個佐證點(引用候選事件的 evidence)
4) 若資訊不足,說明缺什麼

輸入:
question: {{user_question}}
candidates: {{retrieved_events_json}}

輸出:
{
  "answer_zh": "...",
  "best_matches": [
    {"camera_id":"...","time_range":"...","why":"...","evidence":["..."]}
  ],
  "missing_info": ["..."]
}

YOLO + Vision 3.2 實作流程範例

下面是一個「可落地」的最小可行流程(MVP)

Step 0:選擇部署模式

  • 本地端(推薦):Ollama 跑 llama3.2-vision:90b(需強 GPU/多卡)
  • 先用線上 API 驗證流程:NVIDIA 的 Llama-3.2-90B-Vision Instruct(把 pipeline 跑通再回本地)

Step 1:影片 → 影格抽取(Frame Sampling)

策略建議:

  • 監控:每秒 1~2 張 + 場景切分(scene change)
  • 行車:依速度/光流變化調整取樣
  • 事件型:偵測到人/車才加密取樣

Python(OpenCV)骨架:

import cv2
import os

def extract_frames(video_path, out_dir, fps_sample=1):
    os.makedirs(out_dir, exist_ok=True)
    cap = cv2.VideoCapture(video_path)
    fps = cap.get(cv2.CAP_PROP_FPS) or 30
    step = max(int(fps / fps_sample), 1)

    idx = 0
    frame_id = 0
    while True:
        ok, frame = cap.read()
        if not ok:
            break
        if frame_id % step == 0:
            out_path = os.path.join(out_dir, f"frame_{idx:06d}.jpg")
            cv2.imwrite(out_path, frame)
            idx += 1
        frame_id += 1

    cap.release()
    return idx

Step 2:影格 → YOLO / yolo face 偵測(產生 metadata)

可以用:

  • YOLOv8 / YOLOv10 做物件、人、車
  • yolo face(或更強的人臉模型)做臉部框
  • 之後再加 ByteTrack/DeepSORT 形成 track_id

偵測輸出建議格式(JSON):

[
  {"type":"person","conf":0.92,"bbox":[x1,y1,x2,y2]},
  {"type":"car","conf":0.88,"bbox":[x1,y1,x2,y2]},
  {"type":"face","conf":0.81,"bbox":[x1,y1,x2,y2]}
]

Step 3:挑關鍵影格(降低 VLM 成本)

常見挑選法:

  • 有偵測到 person/vehicle 才送 VLM
  • 同一 track_id 每 N 秒只取 1 張代表影格
  • 有事件觸發(越線/闖入/停留過久)才送 VLM

Step 4:送進 Vision 3.2 做語意理解(Ollama 版本)

若你用 Ollama,本質上是:影像 + prompt +(可選)metadata → 回傳描述 JSON。

概念請長這樣:

  1. 把影格讀成 base64 或直接用 Ollama SDK 的 image 欄位
  2. prompt 內含 timestamp、camera_id、detections
  3. 要求模型「輸出 JSON」

依你使用的 Ollama client 調整:

import json

def build_prompt(ts, cam, detections):
    return f"""
你是一個影片偵探AI。請依據影像與偵測結果輸出JSON(繁體中文)。

timestamp: {ts}
camera_id: {cam}
detections: {json.dumps(detections, ensure_ascii=False)}

輸出JSON欄位:
{{
  "timestamp": "...",
  "camera_id": "...",
  "scene_summary": "...",
  "actions": ["..."],
  "suspicious_score": 0,
  "suspicious_reasons": ["..."],
  "summary_zh": "..."
}}
"""

# 送給 Ollama 的地方,依你使用的套件/HTTP API 填上:
# response = ollama.chat(model="llama3.2-vision:90b", messages=[...], images=[...])

Step 5:事件彙整 + 建索引(讓你能「用中文搜影片」)

你至少要存三種資料:

  1. frame-level:每張影格摘要 + detections
  2. event-level:跨影格彙整(start/end、actors、evidence)
  3. 索引:
    • 向量索引(embedding)→ 語意搜尋
    • 關鍵字索引(全文)→ 精準查詢(例如「紅色帽子」「白色車」)

Step 6:查詢(RAG)→ 回答 + 佐證時間軸

流程:

  • 使用者問:「昨天晚上 10 點到 11 點,有沒有人在後門徘徊?」
  • 檢索 event/frame 索引找候選
  • 把候選 evidence 丟給 Prompt 3 做最終回答
  • 回傳:時間範圍 + 相機 + 佐證描述 +(可選)影格連結

你可以直接複製用的「最小資料結構」

frame_record

{
  "frame_id": "cam01_20251216_220501_000123",
  "camera_id": "cam01",
  "timestamp": "2025-12-16T22:05:01",
  "image_path": "/frames/cam01/frame_000123.jpg",
  "detections": [],
  "vlm_json": {},
  "summary_zh": "..."
}

event_record

{
  "event_id": "evt_20251216_cam01_0007",
  "camera_id": "cam01",
  "start_time": "2025-12-16T22:05:00",
  "end_time": "2025-12-16T22:06:30",
  "event_type": "徘徊/闖入/越線/遺留物/群聚",
  "actors": [{"track_id":"p12","type":"person"}],
  "evidence": [
    {"timestamp":"2025-12-16T22:05:15","what":"人物在後門附近停留,反覆靠近門把"}
  ],
  "risk_level": "medium",
  "search_keywords_zh": ["後門","徘徊","可疑人物"]
}

事件規則怎麼定義(越線 / 闖入 / 停留)

A. 共同前置:座標系與 ROI 設定

建議每台攝影機都做一份設定檔(JSON/YAML):

  • ROI 多邊形:例如「後門區域」「禁入區」「收銀台區」
  • Tripwire 線段:例如「門檻線」「圍籬線」
  • 方向向量(可選):判斷越線方向(外→內 / 內→外)

camera_rules.json(示例)

重點:事件規則「不要靠 VLM 猜」,VLM 用在「補語意」,規則用在「可重現、可商業合規」。

B. 越線(Tripwire Crossing)

定義:同一個 track_id 的「腳點/中心點」從線段一側移動到另一側,且跨越時間在合理範圍。

實作要點

  • 取 bbox 底部中心點(foot point)更準:
    foot = ((x1+x2)/2, y2)
  • 記錄上一幀 foot 在直線的「符號」(line side sign)
  • sign 改變(+ → – 或 – → +)= 可能越線
  • 若需要方向:檢查 crossing 前後的位置落在哪個 side

事件輸出

  • event_type: 越線
  • evidence: crossing 發生的 timestamp + 當下影格 id

C. 闖入(Intrusion into Restricted Zone)

定義:目標(person/vehicle)進入「restricted_zone」ROI,且停留超過 N 幀或 N 秒(避免抖動誤判)。

實作要點

  • 以 foot point 是否在多邊形內判斷(比中心點穩)
  • debounce:例如連續 5 幀在區域內才算「進入成立」
  • 退出同理:連續 5 幀不在區域內才算退出

事件輸出

  • event_type: 闖入
  • start_time = 進入成立時間
  • end_time = 退出成立時間

D. 停留 / 徘徊(Loitering)

定義:在指定 zone 內停留超過 min_seconds,且移動速度低於門檻(或移動距離很小但在區域內反覆)。

實作要點

  • per track_id 維護:
    • time_in_zone
    • path_length(foot 點累積距離)
    • avg_speed = path_length / time_in_zone
  • 停留成立條件(示例):
    • time_in_zone >= 20savg_speed <= 15 px/s
  • 可擴充「徘徊」:
    • 在 zone 內來回穿越同一條小線段超過 K 次
    • 或在 zone 內出入(進/出)頻繁

2) 追蹤器接 YOLO(track_id 穩定)

A. 建議組合

  • 偵測:YOLO(person/vehicle/bag…)
  • 追蹤:ByteTrack(速度快、穩)或 DeepSORT(更重但 ReID 強)
  • ID 穩定策略
    1. 只追固定類別:例如只追 person/vehicle(別把雜訊類也追)
    2. 信心值門檻 + NMS:減少跳框
    3. 遮擋容忍:追蹤器允許短暫 lost 再接回
    4. ReID(選配):若常遮擋、跨鏡頭才需要

B. 最小資料流(你系統該長這樣)

frame
  -> YOLO detections: [x1,y1,x2,y2, conf, cls]
  -> Tracker.update(detections)
  -> tracks: [track_id, bbox, cls, conf, state]
  -> event_rules.update(tracks, timestamp)

C. 追蹤輸出建議格式

[
  {"track_id":"p12","type":"person","conf":0.92,"bbox":[...],"foot":[...]}
]

關鍵:後續事件規則都以 track_id 為核心。VLM 的 actors 也引用 track_id,才能串起「時間序列」。

3) 向量索引與查詢(embedding schema、RAG prompt)

A. 你要索引什麼?(兩層:frame-level + event-level)

  • frame-level:細碎、量大,用於精確定位
  • event-level:濃縮、量小,用於查詢與回覆(最重要)

B. 建議的 Embedding Schema(event-level)

每個 event 存:

  • id, camera_id, start_time, end_time
  • event_type
  • actors(含 track_id、類別、特徵詞)
  • summary_zh(VLM + 規則融合)
  • evidence[](時間戳 + 發生描述 + frame_id)
  • keywords_zh[](用於 BM25)
  • embedding(由 summary_zh + keywords_zh 拼成)

event_embedding_text 建議組合

[事件類型] 闖入
[地點] cam01 後門 restricted_zone
[摘要] 一名人物在22:05進入禁入區,停留約35秒後離開。
[關鍵詞] 後門, 禁入區, 闖入, 可疑人物

C. 檢索策略(Hybrid Search)

  1. 關鍵字/BM25:找「後門」「白色車」「紅帽」這種明確詞
  2. 向量/語意:找「徘徊」「鬼鬼祟祟」「疑似踩點」這種抽象語意
  3. 合併 rerank:取 topK events,再丟給 LLM 做最終回答

D. RAG Prompt(查詢 → 回答 + 時間軸 + 點影格)

「可商業用」模板(配之前的 Prompt 3 進化版):

你是影片偵探助理。你會收到:
- 使用者問題 question(繁體中文)
- 候選事件 candidates(由索引檢索得到,含 summary_zh、時間、相機、evidence、frame_link)
你的任務是產生「可行動」的答案:

要求:
1) 先用 3~6 句回答使用者,務必引用 candidates 的內容,不要臆測。
2) 產生時間軸 timeline(依時間排序),每點包含:start~end、camera、event_type、1句描述、frame_link(若有)。
3) 如果 candidates 不足以回答,回 missing_info,並說明需要補哪段時間/哪個相機/哪種事件資料。
4) 全部輸出 JSON,方便前端直接渲染。

輸入:
question: {{question}}
candidates: {{candidates_json}}

輸出 JSON:
{
  "answer_zh": "...",
  "timeline": [
    {
      "camera_id": "...",
      "time_range": "YYYY-MM-DD HH:MM:SS ~ HH:MM:SS",
      "event_type": "...",
      "description": "...",
      "evidence": ["..."],
      "frame_links": ["..."]
    }
  ],
  "missing_info": ["..."]
}

4) 簡單 Web UI(查詢 → 回傳時間軸 → 點開影格)

我給你一個最小 MVP 架構:

A. API 設計(後端)

  • GET /api/search?q=...&from=...&to=...&camera=...
    • 回傳 RAG 結果 JSON(answer_zh + timeline)
  • GET /frames/<frame_id>.jpg
    • 靜態提供影格圖(或簽名 URL)

回傳 JSON(示例)

{
  "answer_zh":"在 22:05~22:06 期間,cam01 後門區域出現 1 次闖入事件...",
  "timeline":[
    {
      "camera_id":"cam01",
      "time_range":"2025-12-16 22:05:00 ~ 22:06:30",
      "event_type":"闖入",
      "description":"人物 p12 進入 restricted_zone 停留約35秒後離開。",
      "evidence":["22:05:15 人物靠近門把","22:06:10 人物離開區域"],
      "frame_links":["/frames/cam01_20251216_220515_000130.jpg"]
    }
  ],
  "missing_info":[]
}

B. 前端(純 HTML+JS 就能跑)

把這段存成 index.html,指向你的 API:

<!doctype html>
<html lang="zh-Hant">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width,initial-scale=1" />
  <title>影片偵探 Demo</title>
  <style>
    body { font-family: system-ui, -apple-system, "Noto Sans TC", sans-serif; margin: 16px; }
    .row { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 12px; }
    input, select, button { padding: 8px; }
    .card { border: 1px solid #ddd; border-radius: 12px; padding: 12px; margin: 10px 0; }
    .timeline-item { cursor: pointer; padding: 10px; border-radius: 10px; }
    .timeline-item:hover { background: #f5f5f5; }
    .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
    img { max-width: 100%; border-radius: 12px; border: 1px solid #eee; }
    pre { background:#fafafa; padding:10px; border-radius:10px; overflow:auto; }
  </style>
</head>
<body>
  <h1>影片偵探 Demo</h1>

  <div class="row">
    <input id="q" size="40" placeholder="例如:後門有沒有人徘徊?白色車出現在哪?" />
    <input id="from" type="datetime-local" />
    <input id="to" type="datetime-local" />
    <select id="camera">
      <option value="">全部相機</option>
      <option value="cam01">cam01</option>
      <option value="cam02">cam02</option>
    </select>
    <button id="btn">查詢</button>
  </div>

  <div class="grid">
    <div>
      <div class="card">
        <h3>答案</h3>
        <div id="answer">尚未查詢</div>
      </div>

      <div class="card">
        <h3>時間軸</h3>
        <div id="timeline"></div>
      </div>

      <div class="card">
        <h3>除錯(回傳 JSON)</h3>
        <pre id="raw">{}</pre>
      </div>
    </div>

    <div>
      <div class="card">
        <h3>影格預覽</h3>
        <div id="frameInfo">點選左側時間軸項目查看影格</div>
        <img id="frame" src="" alt="" style="display:none;" />
      </div>
    </div>
  </div>

<script>
  const $ = (id) => document.getElementById(id);

  function toISOParam(dtLocal) {
    // 直接傳給後端即可(你可在後端再轉時區)
    return dtLocal ? encodeURIComponent(dtLocal) : "";
  }

  function renderTimeline(items) {
    const root = $("timeline");
    root.innerHTML = "";
    if (!items || items.length === 0) {
      root.innerHTML = "<div>沒有找到事件</div>";
      return;
    }
    items.forEach((it, idx) => {
      const div = document.createElement("div");
      div.className = "timeline-item";
      div.innerHTML = `
        <div><b>${it.time_range}</b>|${it.camera_id}|${it.event_type}</div>
        <div>${it.description}</div>
      `;
      div.onclick = () => {
        const link = (it.frame_links && it.frame_links[0]) ? it.frame_links[0] : "";
        $("frameInfo").innerText = link ? `影格:${link}` : "此事件沒有影格連結";
        if (link) {
          $("frame").src = link;
          $("frame").style.display = "block";
        } else {
          $("frame").style.display = "none";
        }
      };
      root.appendChild(div);
    });
  }

  $("btn").onclick = async () => {
    const q = $("q").value.trim();
    const from = toISOParam($("from").value);
    const to = toISOParam($("to").value);
    const camera = encodeURIComponent($("camera").value);

    const url = `/api/search?q=${encodeURIComponent(q)}&from=${from}&to=${to}&camera=${camera}`;
    const res = await fetch(url);
    const data = await res.json();

    $("answer").innerText = data.answer_zh || "";
    renderTimeline(data.timeline || []);
    $("raw").innerText = JSON.stringify(data, null, 2);
  };
</script>
</body>
</html>

最小可跑的 Flask 後端實作(MVP)

最小可跑的 Flask 後端實作(MVP)

  • GET /api/search:用最簡單的「關鍵字 + 語意(可選)」搜尋事件,回傳前端需要的 answer_zh + timeline JSON
  • GET /frames/<path>:提供影格圖片(靜態檔)
  • 內建一份 data/events.json 假資料,讓你不用先把整個 pipeline 做完也能 Demo 起來
  • 前端就用你前面那份 index.html(我也一起放進 static/

你可以直接照以下檔案建立專案。

專案結構

video-detective-mvp/
  app.py
  requirements.txt
  data/
    events.json
  frames/
    cam01_20251216_220515_000130.jpg   (放你自己的影格檔)
  static/
    index.html

requirements.txt

Flask==3.0.3

data/events.json(假資料,可自行新增)

[
  {
    "event_id": "evt_20251216_cam01_0007",
    "camera_id": "cam01",
    "start_time": "2025-12-16T22:05:00",
    "end_time": "2025-12-16T22:06:30",
    "event_type": "闖入",
    "actors": [{"track_id":"p12","type":"person"}],
    "summary_zh": "一名人物進入後門禁入區,停留約35秒後離開。",
    "keywords_zh": ["後門","禁入區","闖入","可疑人物"],
    "evidence": [
      {"timestamp":"2025-12-16T22:05:15","what":"人物靠近門把並進入禁入區"},
      {"timestamp":"2025-12-16T22:06:10","what":"人物離開禁入區"}
    ],
    "frame_links": ["/frames/cam01_20251216_220515_000130.jpg"]
  },
  {
    "event_id": "evt_20251216_cam02_0011",
    "camera_id": "cam02",
    "start_time": "2025-12-16T21:40:10",
    "end_time": "2025-12-16T21:41:05",
    "event_type": "越線",
    "actors": [{"track_id":"v03","type":"vehicle"}],
    "summary_zh": "一輛車跨越門檻線由外往內進入區域。",
    "keywords_zh": ["越線","車輛","進入","門檻線"],
    "evidence": [
      {"timestamp":"2025-12-16T21:40:22","what":"車輛前輪跨越門檻線"}
    ],
    "frame_links": []
  },
  {
    "event_id": "evt_20251216_cam01_0009",
    "camera_id": "cam01",
    "start_time": "2025-12-16T23:10:00",
    "end_time": "2025-12-16T23:11:00",
    "event_type": "停留",
    "actors": [{"track_id":"p33","type":"person"}],
    "summary_zh": "一名人物在後門區域停留約60秒,移動緩慢,疑似徘徊。",
    "keywords_zh": ["後門","停留","徘徊","可疑"],
    "evidence": [
      {"timestamp":"2025-12-16T23:10:20","what":"人物在後門附近停留且來回踱步"}
    ],
    "frame_links": []
  }
]

static/index.html(直接用你前面 UI;這裡給一份可直接用的)

這份會呼叫 /api/search,點時間軸會開影格。

<!doctype html>
<html lang="zh-Hant">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width,initial-scale=1" />
  <title>影片偵探 Demo</title>
  <style>
    body { font-family: system-ui, -apple-system, "Noto Sans TC", sans-serif; margin: 16px; }
    .row { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 12px; }
    input, select, button { padding: 8px; }
    .card { border: 1px solid #ddd; border-radius: 12px; padding: 12px; margin: 10px 0; }
    .timeline-item { cursor: pointer; padding: 10px; border-radius: 10px; }
    .timeline-item:hover { background: #f5f5f5; }
    .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
    img { max-width: 100%; border-radius: 12px; border: 1px solid #eee; }
    pre { background:#fafafa; padding:10px; border-radius:10px; overflow:auto; }
  </style>
</head>
<body>
  <h1>影片偵探 Demo</h1>

  <div class="row">
    <input id="q" size="42" placeholder="例如:後門有沒有人徘徊?禁入區闖入?越線?" />
    <input id="from" type="datetime-local" />
    <input id="to" type="datetime-local" />
    <select id="camera">
      <option value="">全部相機</option>
      <option value="cam01">cam01</option>
      <option value="cam02">cam02</option>
    </select>
    <button id="btn">查詢</button>
  </div>

  <div class="grid">
    <div>
      <div class="card">
        <h3>答案</h3>
        <div id="answer">尚未查詢</div>
      </div>

      <div class="card">
        <h3>時間軸</h3>
        <div id="timeline"></div>
      </div>

      <div class="card">
        <h3>除錯(回傳 JSON)</h3>
        <pre id="raw">{}</pre>
      </div>
    </div>

    <div>
      <div class="card">
        <h3>影格預覽</h3>
        <div id="frameInfo">點選左側時間軸項目查看影格</div>
        <img id="frame" src="" alt="" style="display:none;" />
      </div>
    </div>
  </div>

<script>
  const $ = (id) => document.getElementById(id);

  function renderTimeline(items) {
    const root = $("timeline");
    root.innerHTML = "";
    if (!items || items.length === 0) {
      root.innerHTML = "<div>沒有找到事件</div>";
      return;
    }
    items.forEach((it) => {
      const div = document.createElement("div");
      div.className = "timeline-item";
      div.innerHTML = `
        <div><b>${it.time_range}</b>|${it.camera_id}|${it.event_type}</div>
        <div>${it.description}</div>
      `;
      div.onclick = () => {
        const link = (it.frame_links && it.frame_links[0]) ? it.frame_links[0] : "";
        $("frameInfo").innerText = link ? `影格:${link}` : "此事件沒有影格連結";
        if (link) {
          $("frame").src = link;
          $("frame").style.display = "block";
        } else {
          $("frame").style.display = "none";
        }
      };
      root.appendChild(div);
    });
  }

  $("btn").onclick = async () => {
    const q = $("q").value.trim();
    const from = $("from").value;
    const to = $("to").value;
    const camera = $("camera").value;

    const url = `/api/search?q=${encodeURIComponent(q)}&from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}&camera=${encodeURIComponent(camera)}`;
    const res = await fetch(url);
    const data = await res.json();

    $("answer").innerText = data.answer_zh || "";
    renderTimeline(data.timeline || []);
    $("raw").innerText = JSON.stringify(data, null, 2);
  };
</script>
</body>
</html>

app.py(Flask 後端:搜尋 + 回傳 timeline + 靜態影格)

from __future__ import annotations

import json
import os
from dataclasses import dataclass
from datetime import datetime
from typing import Any, Dict, List, Optional, Tuple

from flask import Flask, jsonify, request, send_from_directory


BASE_DIR = os.path.dirname(os.path.abspath(__file__))
DATA_PATH = os.path.join(BASE_DIR, "data", "events.json")
FRAMES_DIR = os.path.join(BASE_DIR, "frames")


def parse_dt(s: str) -> Optional[datetime]:
    """
    Accepts:
      - "" -> None
      - HTML datetime-local: "2025-12-16T22:05"
      - ISO: "2025-12-16T22:05:00"
    """
    if not s:
        return None
    s = s.strip()
    for fmt in ("%Y-%m-%dT%H:%M", "%Y-%m-%dT%H:%M:%S"):
        try:
            return datetime.strptime(s, fmt)
        except ValueError:
            pass
    # Fallback: try fromisoformat (may accept seconds/microseconds)
    try:
        return datetime.fromisoformat(s)
    except ValueError:
        return None


def load_events() -> List[Dict[str, Any]]:
    if not os.path.exists(DATA_PATH):
        return []
    with open(DATA_PATH, "r", encoding="utf-8") as f:
        return json.load(f)


def dt_in_range(start: datetime, end: datetime, q_from: Optional[datetime], q_to: Optional[datetime]) -> bool:
    if q_from and end < q_from:
        return False
    if q_to and start > q_to:
        return False
    return True


def text_score(query_tokens: List[str], event: Dict[str, Any]) -> int:
    """
    Minimal keyword scoring:
      - match in event_type, summary_zh, keywords_zh, evidence.what
    """
    blob_parts = []
    blob_parts.append(str(event.get("event_type", "")))
    blob_parts.append(str(event.get("summary_zh", "")))
    blob_parts.extend(event.get("keywords_zh", []) or [])
    for ev in event.get("evidence", []) or []:
        blob_parts.append(str(ev.get("what", "")))

    blob = " ".join(blob_parts).lower()
    score = 0
    for t in query_tokens:
        if not t:
            continue
        if t in blob:
            score += 10
    return score


def tokenize_zh(query: str) -> List[str]:
    """
    MVP 簡化版 tokenization:
    - 以空白切
    - 再把常見關鍵詞直接保留(你可改成 jieba/ckip/hfl 分詞)
    """
    q = (query or "").strip().lower()
    if not q:
        return []
    tokens = [x for x in q.replace(",", " ").replace(",", " ").split() if x]
    # 也把一些常見事件詞補入(避免使用者只打「徘徊」但資料用「停留」)
    synonyms = {
        "徘徊": ["停留"],
        "停留": ["徘徊"],
        "闖入": ["入侵", "禁入"],
        "越線": ["跨線", "穿越"],
        "後門": ["門口"]
    }
    expanded = []
    for t in tokens:
        expanded.append(t)
        for s in synonyms.get(t, []):
            expanded.append(s)
    return expanded


def build_timeline_item(e: Dict[str, Any]) -> Dict[str, Any]:
    def fmt_range(s: str, t: str) -> str:
        try:
            sdt = datetime.fromisoformat(s)
            tdt = datetime.fromisoformat(t)
            return f"{sdt:%Y-%m-%d %H:%M:%S} ~ {tdt:%Y-%m-%d %H:%M:%S}"
        except Exception:
            return f"{s} ~ {t}"

    evidence = []
    for ev in e.get("evidence", []) or []:
        ts = ev.get("timestamp", "")
        what = ev.get("what", "")
        if ts and what:
            evidence.append(f"{ts} {what}")
        elif what:
            evidence.append(what)

    return {
        "camera_id": e.get("camera_id", ""),
        "time_range": fmt_range(e.get("start_time", ""), e.get("end_time", "")),
        "event_type": e.get("event_type", ""),
        "description": e.get("summary_zh", ""),
        "evidence": evidence[:3],
        "frame_links": e.get("frame_links", []) or [],
    }


def build_answer(query: str, matched: List[Dict[str, Any]]) -> str:
    if not query.strip():
        return "請輸入查詢內容(例如:後門 徘徊、禁入區 闖入、越線)。"
    if not matched:
        return "沒有找到符合條件的事件。你可以嘗試放寬時間範圍、改用關鍵詞(例如:後門、闖入、停留、越線)。"

    # MVP:用最前面幾筆拼一段可讀答案(之後你可接 LLM/RAG)
    top = matched[:3]
    parts = []
    parts.append(f"找到 {len(matched)} 筆候選事件,最相關的如下:")
    for e in top:
        parts.append(f"- {e.get('camera_id','')}|{e.get('event_type','')}|{e.get('summary_zh','')}")
    return "\n".join(parts)


app = Flask(__name__, static_folder="static", static_url_path="/")


@app.get("/")
def index():
    # 直接用 static/index.html
    return app.send_static_file("index.html")


@app.get("/frames/<path:filename>")
def frames(filename: str):
    # 提供影格圖片
    return send_from_directory(FRAMES_DIR, filename)


@app.get("/api/search")
def api_search():
    q = request.args.get("q", "")
    camera = request.args.get("camera", "").strip()
    q_from = parse_dt(request.args.get("from", ""))
    q_to = parse_dt(request.args.get("to", ""))

    events = load_events()
    tokens = tokenize_zh(q)

    # 先做基本篩選(camera + time range)
    filtered = []
    for e in events:
        if camera and e.get("camera_id") != camera:
            continue
        s = parse_dt(e.get("start_time", "")) or datetime.min
        t = parse_dt(e.get("end_time", "")) or datetime.min
        if not dt_in_range(s, t, q_from, q_to):
            continue
        filtered.append(e)

    # 關鍵字打分排序(MVP)
    scored: List[Tuple[int, Dict[str, Any]]] = []
    for e in filtered:
        score = text_score(tokens, e)
        if tokens and score <= 0:
            continue
        scored.append((score, e))

    scored.sort(key=lambda x: x[0], reverse=True)
    matched = [e for _, e in scored][:20]

    resp = {
        "answer_zh": build_answer(q, matched),
        "timeline": [build_timeline_item(e) for e in matched],
        "missing_info": [] if matched else ["建議擴大時間範圍或指定相機,並使用關鍵字:後門、闖入、停留、越線。"],
    }
    return jsonify(resp)


if __name__ == "__main__":
    os.makedirs(FRAMES_DIR, exist_ok=True)
    app.run(host="0.0.0.0", port=5000, debug=True)

啟動方式

cd video-detective-mvp
python -m venv .venv
# Windows: .venv\Scripts\activate
source .venv/bin/activate

pip install -r requirements.txt
python app.py

打開瀏覽器:http://localhost:5000/ ,MVP 已經能跑 UI + 查詢 + 時間軸 + 點影格。接下來可以逐步替換:

build_answer() 換成 RAG(把 topK candidates 丟給 Llama-3.2-90B-Vision / 或先用任一 LLM)

data/events.json 換成資料庫(SQLite / PostgreSQL)

text_score() 換成 Hybrid Search

BM25(全文索引) + 向量索引(Qdrant/pgvector)

進階實用版本

SQLite 儲存 events(含初始化與匯入)
向量欄位介面(先 stub,未來可無痛換 Qdrant / pgvector)
事件詳情 API(/api/event/<id>,支援多影格 / 證據)
✅ SQLite(事件資料庫)
✅ Qdrant(Hybrid:向量語意 + 關鍵字)
/api/search + /api/event/<id> + /frames/...
✅ 前端 UI(查詢 → 時間軸 → 點事件 → 顯示 evidence + 多影格縮圖/預覽)
✅ ByteTrack「真正接法」我也給你兩種:可跑的簡易 IoU Tracker(保底) + ByteTrack
✅ Flask(單一服務)
✅ 影片抽幀(或直接讀 frames 資料夾)
✅ YOLO 偵測(Ultralytics)
✅ 追蹤(保底可跑 IoU Tracker;之後你只要替換 tracker 類別就能上 ByteTrack)
✅ 事件規則引擎(越線 / 闖入 / 停留)
✅ 自動寫入 SQLite(events.db)
✅ 自動重建 Qdrant 向量索引(可選:Qdrant 有開就做,沒開就跳過)
✅ 前端 UI 立刻能查到事件並點開影格

之後再補

cooldown 去重機制(避免事件爆量)

事件合併(同一個 track 的闖入/停留合併成單一 event,帶 start/end)

把 VLM(Ollama Vision 90B)用於事件摘要:只對「關鍵事件」抽 1~3 張影格做語意補強(成本更可控)


結語:Vision 3.2 90B 為開源影片 AI 帶來新世代

Llama-3.2-90B-Vision 不只是「能看圖的 LLM」,而是讓:

  • 影片搜尋
  • 監控分析
  • 行為理解
  • 事件推論

真正進入 本地端、可控、可商業化 的時代,對於想打造「影片偵探」的團隊而言,它是目前開源世界中最具實力的選擇之一

參考資料

https://mermaid.live


Deprecated: explode(): Passing null to parameter #2 ($string) of type string is deprecated in C:\inetpub\raintips\wp-content\themes\Divi\core\functions.php on line 1613

Deprecated: explode(): Passing null to parameter #2 ($string) of type string is deprecated in C:\inetpub\raintips\wp-content\themes\Divi\core\functions.php on line 1613

Deprecated: explode(): Passing null to parameter #2 ($string) of type string is deprecated in C:\inetpub\raintips\wp-content\themes\Divi\core\functions.php on line 1613
Manus 沒邀請碼怎麼辦?用 OpenManus 本地免費部署 Ollama 模型,三分鐘搞定

Manus 沒邀請碼怎麼辦?用 OpenManus 本地免費部署 Ollama 模型,三分鐘搞定


Deprecated: explode(): Passing null to parameter #2 ($string) of type string is deprecated in C:\inetpub\raintips\wp-content\themes\Divi\core\functions.php on line 1613

Deprecated: explode(): Passing null to parameter #2 ($string) of type string is deprecated in C:\inetpub\raintips\wp-content\themes\Divi\core\functions.php on line 1613

Deprecated: explode(): Passing null to parameter #2 ($string) of type string is deprecated in C:\inetpub\raintips\wp-content\themes\Divi\core\functions.php on line 1613

🚀 1. 本地端完美對接 Ollama AI 模型

OpenManus 最大的亮點在於能與目前最流行的 Ollama 本地端 AI 大模型平台進行完美整合。

  • Ollama 是一個輕量、高效的 AI 模型管理工具,讓你可以輕鬆在自己的電腦上運行各種強大的大模型(如 Llama3、Qwen、DeepSeek 系列模型等)。
  • OpenManus 透過 Ollama API 與這些模型無縫互動,你能輕易在本地體驗到媲美線上服務的智慧功能,並保護個人隱私。

💻 2. 跨平台支援 Windows、Mac、Linux

無論你使用哪個平台,OpenManus 都有完整的跨平台支援,讓你輕鬆安裝與運行:

  • Windows 用戶可透過 Conda 或 Docker 快速部署。
  • macOS 用戶可以使用 Homebrew 或直接透過終端機運行。
  • Linux 用戶則能自由選擇 Docker 或直接透過原生方式安裝。

🎯 3. 無需邀請碼,即裝即用!

不同於原始封閉的 Manus 需要透過邀請碼才能使用,OpenManus 堅持完全開源與自由的精神。
無須註冊、無須邀請碼,直接部署到自己的電腦,立即開始使用,毫無限制,這就是開源社群給予大家最棒的禮物。


如何快速部署 OpenManus?(以 Windows 為例)

只需幾個簡單步驟,即可享受本地端 AI 大模型:

建立 Conda 環境:

conda create -n openmanus python=3.12
conda activate openmanus

Git OpenManus 專案:

git clone https://github.com/mannaandpoem/OpenManus.git
cd OpenManus

安裝所需依賴:

pip install -r requirements.txt

修改設定檔(config.toml):

cp config/config.example.toml config/config.toml

config.toml的內容如下,可以參考後修改

# Global LLM configuration
#[llm]
# model = "claude-3-7-sonnet-20250219"        # The LLM model to use
# base_url = "https://api.anthropic.com/v1/"  # API endpoint URL
# api_key = "YOUR_API_KEY"                    # Your API key
# max_tokens = 8192                           # Maximum number of tokens in the response
# temperature = 0.0                           # Controls randomness

# [llm] #AZURE OPENAI:
# api_type= 'azure'
# model = "YOUR_MODEL_NAME" #"gpt-4o-mini"
# base_url = "{YOUR_AZURE_ENDPOINT.rstrip('/')}/openai/deployments/{AZURE_DEPOLYMENT_ID}"
# api_key = "AZURE API KEY"
# max_tokens = 8096
# temperature = 0.0
# api_version="AZURE API VERSION" #"2024-08-01-preview"

[llm] #OLLAMA:
api_type = 'ollama'
model = "llama3.2"
base_url = "http://localhost:11434/v1"
api_key = "ollama"
max_tokens = 4096
temperature = 0.0

# Optional configuration for specific LLM models
#[llm.vision]
#model = "claude-3-7-sonnet-20250219"        # The vision model to use
#base_url = "https://api.anthropic.com/v1/"  # API endpoint URL for vision model
#api_key = "YOUR_API_KEY"                    # Your API key for vision model
#max_tokens = 8192                           # Maximum number of tokens in the response
#temperature = 0.0                           # Controls randomness for vision model

[llm.vision] #OLLAMA VISION:
api_type = 'ollama'
model = "llama3.2-vision"
base_url = "http://localhost:11434/v1"
api_key = "ollama"
max_tokens = 4096
temperature = 0.0

# Optional configuration for specific browser configuration
# [browser]
# Whether to run browser in headless mode (default: false)
#headless = false
# Disable browser security features (default: true)
#disable_security = true
# Extra arguments to pass to the browser
#extra_chromium_args = []
# Path to a Chrome instance to use to connect to your normal browser
# e.g. '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'
#chrome_instance_path = ""
# Connect to a browser instance via WebSocket
#wss_url = ""
# Connect to a browser instance via CDP
#cdp_url = ""

# Optional configuration, Proxy settings for the browser
# [browser.proxy]
# server = "http://proxy-server:port"
# username = "proxy-username"
# password = "proxy-password"

# Optional configuration, Search settings.
# [search]
# Search engine for agent to use. Default is "Google", can be set to "Baidu" or "DuckDuckGo".
# engine = "Google"

啟動 OpenManus 服務:

python main.py

之後打開瀏覽器就可以了

測試 OpenManus :

可以輸入請他使用瀏覽器看某一個網站,並且執行SEO策略

打開 https://rain.tips/ 並且給予SEO的建議,並且把建議存放在桌面上.txt的文件

補充資料

Github


Deprecated: explode(): Passing null to parameter #2 ($string) of type string is deprecated in C:\inetpub\raintips\wp-content\themes\Divi\core\functions.php on line 1613

Deprecated: explode(): Passing null to parameter #2 ($string) of type string is deprecated in C:\inetpub\raintips\wp-content\themes\Divi\core\functions.php on line 1613

Deprecated: explode(): Passing null to parameter #2 ($string) of type string is deprecated in C:\inetpub\raintips\wp-content\themes\Divi\core\functions.php on line 1613
512GB Mac Studio 完美運行 DeepSeek R1!VRAM 調整攻略公開

512GB Mac Studio 完美運行 DeepSeek R1!VRAM 調整攻略公開


Deprecated: explode(): Passing null to parameter #2 ($string) of type string is deprecated in C:\inetpub\raintips\wp-content\themes\Divi\core\functions.php on line 1613

Deprecated: explode(): Passing null to parameter #2 ($string) of type string is deprecated in C:\inetpub\raintips\wp-content\themes\Divi\core\functions.php on line 1613

Deprecated: explode(): Passing null to parameter #2 ($string) of type string is deprecated in C:\inetpub\raintips\wp-content\themes\Divi\core\functions.php on line 1613

超大型語言模型(LLM)成為科技界矚目的焦點,以前這類模型通常需要極高的硬體門檻,要很多的 GPU 才能達成(需要好幾百萬),難以在本地設備上流暢運行。然而,現在配備 512GB 超大記憶體的 Mac Studio,約33萬台幣,就能輕鬆駕馭滿血版 DeepSeek R1,讓個人或企業用戶都能輕鬆享受超大型語言模型帶來的豐富應用價值!

為何 512GB 就足夠跑 DeepSeek R1?

DeepSeek R1 是一款擁有超過 6710 億參數的超級大型語言模型,理論上需超過 400GB 以上記憶體空間才能順暢載入。然而,DeepSeek R1 採用了特殊的 Mixture of Experts (MoE) 架構,儘管整體模型規模龐大,但實際上單次推理只會激活約 370 億參數,大幅減少記憶體的實際使用需求,讓 512GB 記憶體的 Mac Studio 就能輕鬆駕馭。

關鍵技巧:調整 VRAM 配置,釋放更大的 GPU 資源

Mac Studio 使用的是統一記憶體架構(Unified Memory),系統自動分配 GPU 使用的 VRAM 空間。預設情況下,VRAM是有限制的,不足以負荷 DeepSeek R1 這樣龐大的語言模型,但使用者可以透過調整系統參數,自由設定 GPU 的 VRAM 配置,以達到最大效能:

以下是關鍵指令:

sudo sysctl iogpu.wired_limit_mb=448000

透過這項設定,系統的 GPU VRAM 即可輕鬆擴展到 448GB,滿足 DeepSeek R1 等超大型模型的嚴苛需求,真正發揮 512GB 記憶體 Mac Studio 的硬體潛力。

⚠️ 貼心提醒:
調整 VRAM 前,建議備份重要資料。修改設定可能影響系統穩定性,請謹慎操作。

EXO 工具:連接多台 Mac,建立強大的分散式計算環境

如果你需要更強大的算力,還可以透過開源的 EXO 專案,將多台 Mac 電腦串聯起來,組成超強大的本地計算集群,以協同運行 DeepSeek R1 這類超大型語言模型。

透過 EXO,你可以:

  • 將多台 Mac Studio 連結成計算網絡。
  • 有效分散模型推理負載,提升整體效能。
  • 進一步降低單機的運算負擔,確保持續穩定運作。

這個方法尤其適合專業研究團隊、企業內部部署,甚至是有進階 AI 運算需求的開發者。

參考資料

https://zenn.dev/robustonian/articles/apple_silicon_vram