2026年4月17日 星期五

Dify + LangBot + Tg + Firefly III + Firefly III mcp + llamacp + gemma4-e2b-it 建置一個家庭記帳

Dify + LangBot + Tg + Firefly III + Firefly III mcp + llamacp + gemma4-e2b-it 建置一個家庭記帳

這個組合有點長,不過過程很簡單,只需要你安裝 docker,後面通通都是一路順暢的,不管如何,我只能先假設你已經有安裝 docker,我的執行環境是 windows

安裝Firefly III

下面給出 Firefly IIIdocker-compose.yml 內容,這可以從官方文件中下載取得:

services:
  app:
    image: fireflyiii/core:latest
    hostname: app
    container_name: firefly_iii_core
    restart: always
    volumes:
      - firefly_iii_upload:/var/www/html/storage/upload
    env_file: .env
    networks:
      - firefly_iii
    ports:
      - 8099:8080
    depends_on:
      - db
  db:
    image: mariadb:lts
    hostname: db
    container_name: firefly_iii_db
    restart: always
    env_file: .db.env
    networks:
      - firefly_iii
    volumes:
      - firefly_iii_db:/var/lib/mysql
  cron:
    #
    # To make this work, set STATIC_CRON_TOKEN in your .env file or as an environment variable
    # The STATIC_CRON_TOKEN must be *exactly* 32 characters long
    # Use this URL for inspiration: https://www.random.org/strings/?num=1&len=32&digits=on&upperalpha=on&loweralpha=on&unique=on&format=html&rnd=new
    #
    image: alpine
    restart: always
    container_name: firefly_iii_cron
    env_file: .env
    command: ["sh", "-c", "apk add tzdata && \
      (ln -s /usr/share/zoneinfo/$$TZ /etc/localtime || true) && \
      echo \"0 3 * * * wget -qO- http://app:8080/api/v1/cron/$$STATIC_CRON_TOKEN;echo\" \
      | crontab - && \
      crond -f -L /dev/stdout"]
    networks:
      - firefly_iii
    depends_on:
      - app

volumes:
   firefly_iii_upload:
   firefly_iii_db:

networks:
  firefly_iii:
    driver: bridge

官方文件有特別提到要兩個 env 檔,記得下載,然後參數做相對應的調整:

Download configuration files
There are two configuration files you need to run this Docker Compose file. Download all files and save them in the same folder as the Docker Compose file.

The first file contains Firefly III variables and can be downloaded from the Firefly III repository. Save it as a new file called .env.
The second file contains the database variables and can be downloaded from the Docker repository. Save it as a new file called .db.env.

調整完畢之後就可以執行 docker compose up -d,第一個登入的帳號就是管理者,然後你可以直接把註冊取消,這樣子就不怕有人偷偷再去註冊帳號。

登入之後系統會要求設置新的帳戶:
image

語系問題不用擔心,按下 Submit 之後就會轉成繁體中文了,不過前提是你要記得選擇正確語系就是。

帳戶中可以看到有一個預設的 現金皮夾 跟我們剛剛設置的 兆豐銀行,這依各自的現況處理就好:
image

取得個人 token

稍後我們要配置 firefly iii mcp,這需要先取得個人的 token,選項\個人檔案\OAuth,然後點擊『建立新權杖』,名稱自己設置,很長的一串,不要懷疑,就是這一串,複製出來,然後放記事本等等用。

image

啟動 firefly iii mcp

version: '3.8'

services:
  mcp:
    image: node:20-alpine
    container_name: firefly_mcp
    restart: unless-stopped
    ports:
      - "3100:3000"
    command: >
      sh -c "npx --yes @firefly-iii-mcp/server
      --pat $$FIREFLY_III_PAT
      --baseUrl $$FIREFLY_III_BASE_URL
      --port 3000
      --preset full
      --logLevel info
      --tools transactions,accounts,summary"
    environment:
      FIREFLY_III_BASE_URL: "http://192.168.1.243:8099"
      FIREFLY_III_PAT: 貼上你自己的token

啟動 llamacpp

我的習慣是先自己手動下載模型,下載的部份可以安裝 hf cli來處理,很方便,可以參考官方文檔先安裝,然後下載模型:

hf download unsloth/gemma-4-E2B-it-GGUF gemma-4-E2B-it-Q8_0.gguf --local-dir D:\models\gemma-4-E2B

模型放那就自行決定,我是放在D:\models\gemma-4-E2B

然後就啟動 llamacpp

docker run --name=llama-cpp --volume D:/models/gemma-4-E2B:/mnt/model --network=bridge --workdir=/app -p 11435:8080 --runtime=runc --detach=true ghcr.io/ggml-org/llama.cpp:server-cuda -m /mnt/model/gemma-4-E2B-it-Q8_0.gguf --host 0.0.0.0 --port 8080 -ngl 99 --jinja --reasoning off

啟動 Dify

官方說明:https://docs.dify.ai/zh/self-host/quick-start/docker-compose

首先把程式庫下載:

git clone --branch "$(curl -s https://api.github.com/repos/langgenius/dify/releases/latest | jq -r .tag_name)" https://github.com/langgenius/dify.git

切換目錄:

cd dify/docker

複製文件:

cp .env.example .env

啟動:

docker compose up -d

第一個註冊 dify 的帳號就是管理者,然後登入之後就先做模型的配置,先點擊右上的人名,然後點擊設置:
image

點擊模型供應商,先選擇安裝 OpenAI-API-compatible
image

這邊你也可以安裝ollama,不過效率上的問題,我還是比較愛 llamacpp 或是 llama-swap,有試著安裝 vllm,不過響應的是未支援 gemma4 系列模型,我就沒有再試下去了。

接下來就可以新增模型,把路由指向剛剛啟動的 llamacpp,前面的名稱我是設置 gemma4
image

不要讓它思考,它思考之後是一團糟。

設置 agent

先把剛剛設置的 firefly mcp加進來,點擊『工具』,然後『新增MCP』
image

路由指向啟動 mcp 服務的位址:
image

成功的話就會看到下面圖示:
image

接下來設置 agent,點擊『工作室』,點『對於初學者』,然後點擊『Agent』:
image

點擊『工具』->『新增』->『CurrentTime』->『CurrentTime』
點擊『工具』->『新增』->『MCP』:
image

把工具加一加應該就會是下面這個樣子:
image

請注意,那個『CurrentTime』很重要,一定要加進來,不然機器不會知道今天是什麼時候。

然後提示詞是這樣的:

你是記帳助手。每次記帳前,必須先呼叫 list_account 取得帳戶清單,再用語意對應 source_name。

## 固定執行順序

### 記帳(三步,缺一不可)
Step 1:呼叫 Current Time 取得當前時間
Step 2:呼叫 list_account 取得帳戶清單
        → 只能選 type 為 asset 的帳戶作為 source_name
        → 禁止選 type 為 cash account / expense account / revenue account 的帳戶
Step 3:呼叫 store_transaction,date 使用 Step 1 結果,source_name 使用 Step 2 結果
語意對應規則:
現金/錢包/cash → 選清單中含「現金」或「cash」的帳戶
銀行/帳戶 → 選清單中含銀行名稱的帳戶
信用卡/card → 選清單中含「信用卡」或「card」的帳戶
沒有明確提到 → 選清單中第一個 asset 類型帳戶

### 查詢摘要
觸發詞:本月、這個月、花了多少、支出統計
呼叫:get_basic_summary
參數:
- start: 本月第一天(YYYY-MM-01)
- end: 今天(YYYY-MM-DD)

### 查交易列表
觸發詞:查帳、列出、最近幾筆
呼叫:list_transaction
參數:limit=10

## store_transaction 參數規則

呼叫時必須使用以下 JSON 格式:

{
  "transactions": [
    {
      "date": "從 Current Time 取得的時間,格式 YYYY-MM-DDTHH:MM:SS+08:00",
      "description": "用途描述",
      "source_name": "從 list_account 結果中取得的真實名稱",
      "type": "withdrawal / deposit / transfer",
      "amount": "金額數字",
      "category_name": "餐飲 / 交通 / 購物 / 居家 / 收入 / 其他",
      "destination_name": "店家或收款對象名稱"
    }
  ]
}

注意:
- 必須包在 transactions 陣列裡
- 不要外層再包 store_transaction 或 requestBody
- 直接傳遞這個 JSON 物件作為工具參數

## 缺資訊時只問一個問題
缺金額 → 「金額是多少?」
缺帳戶且語意無法判斷 → 「用哪個帳戶?」



## 禁止行為
- 禁止在未呼叫 list_account 前就呼叫 store_transaction
- 禁止自創帳戶名稱
- 禁止猜測金額
- 禁止一次呼叫多個工具
- 禁止輸出工具呼叫以外的說明文字
- 禁止省略 category_name,無法判斷時預設「其他」

最後,你的模型記得指向剛剛配置的模型:
image

現在就可以來測試了:
image

回頭確認 fireflly 是否確實有這筆記錄:
image

工具調用偶有問題,真的是要多次調整就是,這就自己測試的時候看發生什麼事,從日誌確認問題,然後就丟給其它語言模型調整就好。

確認之後就可以發佈這個 dify 的服務,點擊發佈,然後發佈更新:
image

發佈之後就再點擊發佈,然後點擊『訪問API』,點擊『API金鑰』:
image

建立取得 token,然後先貼到你的記事本,等等要用。

安裝 LangBot

官方程式庫:https://github.com/langbot-app/LangBot

直接把這個程式庫 clone 到地本端,然後執行 docker compose up -d

git clone https://github.com/langbot-app/LangBot
cd LangBot/docker
docker compose up -d

登入之後,在機器人的下拉旁有一個小小的『+』:
image

很重要的是,你必需要先用 teletram 自己跟 botfather 要一個機器人的 token
image

建立好機器人之後,先設置一個流水線,一樣的,在『流水線』旁有一個小『+』,點擊,然後配置:
image

  1. 名稱自己設置
  2. AI 能力的部份,把 Runner 修正為 Dify Service API,然後路由指向 dify 的服務,API Key 就是剛剛那串

設置好之後就可以儲存,然後把這流水線跟機器人做綁定:
image

綁定後記得按下保存。

測試

好了,現在,我在 tg 上測試打一句『在星巴巴買咖啡200元,現金支付』

我們看 langbot 的日誌:
image

然後 dify 的日誌:
image

最後 firefly 的記錄:
image

完成!

2026年2月5日 星期四

github-copilot 初體驗

Github Copilit-sdk初體驗 - 官方範例排雷

環境

  • 作業系統:windows 10 64bit
  • python:3.13
  • copilot cli:GitHub Copilot CLI 0.0.402.
  • github-copilot-sdk:0.1.21

請記得安裝好相關需求套件,copilot cli一定要安裝,官方文件來看似乎並沒有一定要登入,如果有自己架設好的推論資源也可以直接使用,不過這次的測試是基於登入copilot-plus帳號所使用。

官方文件:https://github.com/github/copilot-sdk/blob/main/docs/getting-started.md

官方範例如下:

import asyncio
from copilot import CopilotClient

async def main():
    client = CopilotClient()
    await client.start()

    session = await client.create_session({"model": "gpt-4.1"})
    response = await session.send_and_wait({"prompt": "What is 2 + 2?"})

    print(response.data.content)

    await client.stop()

asyncio.run(main())

上面程式碼執行之後立即爆雷:

Traceback (most recent call last):
  File "D:\codeProject\py_sdk\main.py", line 25, in <module>
    asyncio.run(main())           
    ~~~~~~~~~~~^^^^^^^^
  File "C:\Users\marty.chen\AppData\Roaming\uv\python\cpython-3.13.2-windows-x86_64-none\Lib\asyncio\runners.py", line 195, in run
    return runner.run(main)
           ~~~~~~~~~~^^^^^^
  File "C:\Users\marty.chen\AppData\Roaming\uv\python\cpython-3.13.2-windows-x86_64-none\Lib\asyncio\runners.py", line 118, in run
    return self._loop.run_until_complete(task)
           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^
  File "C:\Users\marty.chen\AppData\Roaming\uv\python\cpython-3.13.2-windows-x86_64-none\Lib\asyncio\base_events.py", line 725, in run_until_complete
    return future.result()
           ~~~~~~~~~~~~~^^
  File "D:\codeProject\py_sdk\main.py", line 16, in main
    await client.start()
  File "D:\codeProject\py_sdk\.venv\Lib\site-packages\copilot\client.py", line 241, in start   
    await self._start_cli_server()
  File "D:\codeProject\py_sdk\.venv\Lib\site-packages\copilot\client.py", line 1068, in _start_cli_server
    self._process = subprocess.Popen(
                    ~~~~~~~~~~~~~~~~^
        args,
        ^^^^^
    ...<5 lines>...
        env=env,
        ^^^^^^^^
    )
    ^
  File "C:\Users\marty.chen\AppData\Roaming\uv\python\cpython-3.13.2-windows-x86_64-none\Lib\subprocess.py", line 1038, in __init__
    self._execute_child(args, executable, preexec_fn, close_fds,
    ~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                        pass_fds, cwd, env,
                        ^^^^^^^^^^^^^^^^^^^
    ...<5 lines>...
                        gid, gids, uid, umask,
                        start_new_session, process_group)
                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\marty.chen\AppData\Roaming\uv\python\cpython-3.13.2-windows-x86_64-none\Lib\subprocess.py", line 1550, in _execute_child
    hp, ht, pid, tid = _winapi.CreateProcess(executable, args,
                       ~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^
                             # no special security
                             ^^^^^^^^^^^^^^^^^^^^^
    ...<4 lines>...
                             cwd,
                             ^^^^
                             startupinfo)
                             ^^^^^^^^^^^^
FileNotFoundError: [WinError 2] 系統找不到指定的檔案。

可參考issue內的處理說明。

調整如下:

import os
import shutil
import asyncio
from copilot import CopilotClient

cli_path = os.environ.get('COPILOT_CLI_PATH') or shutil.which('copilot')
if not cli_path:
    raise RuntimeError(
        "copilot CLI not found. Set COPILOT_CLI_PATH or install the CLI and ensure it's on PATH."
    )   

async def main():
    client_opts = {'cli_path': cli_path}
    client = CopilotClient(client_opts)

    try:
        await client.start()
    
        # 嚐試確認登入狀態
        try:
            auth = await client.get_auth_status()
            print(f"Authentication Status: {auth.isAuthenticated}")
        except Exception as e:
            print(f"Authentication check failed: {e}")

        # 取得可用模型清單
        try:
            models = await client.list_models()
            print(f"\n{'='*60}")
            print("Available Models Numbers: ", len(models))
            print(f"{'='*60}\n")

            for model in models:
                print(f"- {model.name} (ID: {model.id})")
        except Exception as e:
            print(f"Failed to list models: {e}")
        
        # 執行官方文件的範例測試
        try:
            session = await client.create_session({'model': 'gpt-4.1'})
            response = await session.send_and_wait({'prompt': 'What is 2+2?'})
            print(f"\nResponse from model:\n{response.data.content}\n")
        except Exception as e:
            print(f"Session interaction failed: {e}")   
    finally:
        await client.stop()

    

if __name__ == "__main__":
    asyncio.run(main())         

總算,成功了:

Authentication Status: True

============================================================
Available Models Numbers:  14
============================================================

- Claude Sonnet 4.5 (ID: claude-sonnet-4.5)
- Claude Haiku 4.5 (ID: claude-haiku-4.5)
- Claude Opus 4.5 (ID: claude-opus-4.5)
- Claude Sonnet 4 (ID: claude-sonnet-4)
- Gemini 3 Pro (Preview) (ID: gemini-3-pro-preview)
- GPT-5.2-Codex (ID: gpt-5.2-codex)
- GPT-5.2 (ID: gpt-5.2)
- GPT-5.1-Codex-Max (ID: gpt-5.1-codex-max)
- GPT-5.1-Codex (ID: gpt-5.1-codex)
- GPT-5.1 (ID: gpt-5.1)
- GPT-5 (ID: gpt-5)
- GPT-5.1-Codex-Mini (ID: gpt-5.1-codex-mini)
- GPT-5 mini (ID: gpt-5-mini)
- GPT-4.1 (ID: gpt-4.1)

Response from model:
2+2 equals 4.

結論

還算是方便,這個坑花了我幾個小時處理,也不知道是windows特有的,還是linux也會有,總之,我爬出來了。

後續的官方範例只要基於調整後的模式套用就可以一路順暢。

2026年1月2日 星期五

DeepSeek-OCR:Context-Aware Optical Character Recognition 論文翻譯

DeepSeek-OCR:Context-Aware Optical Character Recognition 論文翻譯

📄 完整翻譯與逐段說明請見 HackMD
👉 https://hackmd.io/@shaoeChen/ByW0qjA1Zx


論文背景與研究動機

傳統 OCR 系統在處理複雜文件(如長文本、多欄排版、數學符號、程式碼或低品質掃描圖像)時,往往僅依賴局部視覺資訊,忽略了更高層次的語義與上下文關係,導致辨識結果在實際應用中仍有明顯限制。

DeepSeek-OCR 旨在解決這一問題,核心目標是將「上下文理解」納入 OCR 的建模過程,讓模型不只是辨識字形,而是能結合語意與結構來提升整體辨識準確率。


核心方法概念

DeepSeek-OCR 的主要特色可概括為以下幾點:

  • Context-Aware 設計
    不再將 OCR 視為純粹的影像到文字轉換,而是引入上下文資訊,使模型能利用前後語意、版面結構與語言規律來輔助判斷。

  • 視覺與語言特徵的深度融合
    透過多模態架構,同時建模影像特徵與文字語意,減少單一視覺訊號帶來的不確定性。

  • 對複雜文本場景的適應性
    特別針對長文、技術文件與高資訊密度內容進行優化,提升在真實世界應用場景中的穩定度。


實驗結果與論文貢獻

論文實驗顯示,DeepSeek-OCR 在多項 OCR benchmark 上,相較於傳統方法與部分現有模型,能在整體準確率與穩定性上取得明顯提升,尤其是在以下場景中表現突出:

  • 長上下文文本

  • 排版結構複雜的文件

  • 語意依賴性高的內容(如技術文件、論文、程式碼)

這些結果顯示,引入上下文理解對 OCR 任務具有實質幫助。


總結

DeepSeek-OCR 提供了一個值得關注的研究方向:
OCR 不應只關注字元級的視覺辨識,而應結合語意與上下文理解。

對於從事文件理解(Document Understanding)、資訊抽取(Information Extraction)或多模態模型研究的讀者而言,這篇論文在方法設計與問題切入點上,都具有相當的參考價值。

📌 若你想閱讀 完整論文翻譯、技術細節與逐段解析,請前往我的 HackMD:
👉 https://hackmd.io/@shaoeChen/ByW0qjA1Zx

2025年12月17日 星期三

WhisperLiveKit + 聯發科Breeze ASR 25

WhisperLiveKit + 聯發科Breeze ASR 25

參考來源

WhisperLiveKit
Breeze-ASR-25

設置流程

  1. 首先clone案至本機
git clone https://github.com/QuentinFuxa/WhisperLiveKit.git
  1. 調整Dockerfile,加入安裝safetensors
...(前略)
# timeout/retries for large torch wheels
RUN pip3 install --upgrade pip setuptools wheel && \
    pip3 --disable-pip-version-check install --timeout=120 --retries=5 \
        --index-url https://download.pytorch.org/whl/cu129 \
        torch torchaudio \
    || (echo "Initial install failed — retrying with extended timeout..." && \
        pip3 --disable-pip-version-check install --timeout=300 --retries=3 \
            --index-url https://download.pytorch.org/whl/cu129 \
            torch torchvision torchaudio)
# 加入這段            
RUN pip3 install safetensors 
...(後略)
COPY . .
  1. 產出image,我加入-safetensors來做自己版本識別使用
docker build -t wlk-safetensors .
  1. 安裝huggingface cli
curl -LsSf https://hf.co/cli/install.sh | bash
  1. 利用huggingface cli下載聯發科Breeze ASR
    請注意,--local-dir後面請調整為你的實際保存位置
env PATH="$HOME/.local/bin:$PATH" hf download   MediaTek-Research/Breeze-ASR-25   --local-dir /home/marty/large_files/Breeze-ASR-25
  1. docker run啟動容器
    data volume(-v) 與 port(-p) 的部份請根據你的實際狀況調整
docker run --rm --gpus all   -p 8081:8000   -v /home/marty/large_files/Breeze-ASR-25:/models/Breeze-ASR-25   -v /home/marty/.cache/whisper:/root/.cache/whisper   -v /home/marty/.cache/huggingface:/root/.cache/huggingface   wlk-safetensors:latest   --backend whisper   --model-path /models/Breeze-ASR-25   --language zh

啟動之後就會開始下載一個base.pt,大約130mb,然後就會看到服務啟動的訊息:

docker run --rm --gpus all   -p 8081:8000   -v /home/marty/large_files/Breeze-ASR-25:/models/Breeze-ASR-25   -v /home/marty/.cache/whisper:/root/.cache/whisper   -v /home/marty/.cache/huggingface:/root/.cache/huggingface   wlk-safetensors:latest   --backend whisper   --model-path /models/Breeze-ASR-25   --language zh
INFO:     Started server process [1]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)

這邊看到的8000是容器內的對應port,現在你只要打開網頁,然後連結到你啟動服務的8081port就可以開始測試了。

記得要有麥克風就是。

2025年12月10日 星期三

Spec Kit實作記錄

Spec Kit實作記錄

hackmd連結

Spec Kit的出現似乎讓vibe coding的未來出現一絲光明,即使這名詞的發明者,AI大神Andrej Karpathy,最近也出來說明這並不可行,但是對於一些簡單的專案來說應該還是有幫助才是。

我個人仍然是認為,寫程式重視底韻,你必需要有一定的知識才能有辦法去驅動人工智慧,不然你根本連AI在幹嘛都不知道,仔細想想,這是很恐怖的一件事。

也許你認為在什麼都不懂的情況下可以用自然語言的方式生成一個網頁或是應用程式非常潮,但是在沒有一定知識的情況下你也只知道就是生成一個你想像中的結果,這個結果有沒有什麼安全性的問題可能連自己都是一問三不知了。

最新心得

最近利用SDD的概念開發一個工作日誌系統,心得上就是,這實在是很花費時間,也許就如不少大神說的,這真的是陷入MARKDOWN地獄,還不如一個功能好好的說明讓AGENT開發。

實作過程中有發現,即使有TASK LIST,AGENT還是會在沒有完成的情況下勾選完成,這過程也確實是很花時間的在DEBUG,而且為了遵守流程,可能還需要回頭從specify或是task來重新執行。

上下文的部份,也許我們可以更依賴AGENTS.md(https://agents.md/)或是COPILOT本身所自帶的instructionprompt

當然也有可能是因為我是在"微軟大戰程式碼"(vscode)中使用的,導致SDD的效能沒有辦法被完整的釋放,改天有機會再來試著在COPILOT CLI上執行看看,也許會有不一樣的感受才是。

技術債會累積,即使使用SDD仍然會有相同的問題,不過也有可能是我不會使用,不一定是Spec Kit所造成的就是。

步驟

官方連結

在Spec-Kit的官方說明中明確的列出幾個指令,簡單易懂,就跟叫叫ABC一樣,其中Essential Commands是過程中必需執行的指令,而Optional Commands是過程中你可以選擇要不要執行的指令。

我看保哥在一個公開談話中的demo是在specify階段中直接給定PRD文件,這部份當然也是可以另外利用LLM來生成。

🧭 Essential Commands

指令 說明
/speckit.constitution 建立或更新專案的治理原則與開發指導方針
/speckit.specify 定義你想要建構的內容(需求與使用者故事)
/speckit.plan 依據選定的技術棧建立技術實作計畫
/speckit.tasks 產生可執行的實作任務清單
/speckit.implement 依照計畫執行所有任務,完成功能建置

🧩 Optional Commands

指令 說明
/speckit.clarify 釐清未明確定義的部分(建議在 /speckit.plan 之前執行;原名 /quizme
/speckit.analyze 進行跨產物一致性與覆蓋度分析(建議在 /speckit.tasks 之後、/speckit.implement 之前執行)
/speckit.checklist 生成自訂品質檢查清單,驗證需求的完整性、清晰度與一致性(類似「英文單元測試」)

⚙️ Environment Variables

變數 說明
SPECIFY_FEATURE 用於非 Git 儲存庫的功能偵測覆蓋。設定為功能目錄名稱(例如:001-photo-albums)以指定當前工作的功能。
注意: 在使用 /speckit.plan 或後續指令前,必須於代理環境中設定此變數。

指令簡單說明

/speckit.constitution

在這階段主要定義整個專案過程中不可違逆,必需完全遵守的規則,像我會要求所有文件皆以正體中文書寫,開發過程必需遵守MVP(最小開發原則)

實作記錄

開發工具

我採用的測試開發工具是KILO CODE,然後首次儲值是10美金送20美金,搭配的模型是deepseek 3.2claude haiku 4.5,一個便宜一個好用,主力是deepseek

文件導讀工具

目前對於新的技術我的作法都是直接將所有文件利用notebookllm整理,自己會快速的看過一次,後續有問題就直接在notebookllm中詢問。

這樣做的好處在於問題不會發散也不會有所謂的幻覺出現,因為notebookllm的問題回應始終是根據你所給定的文件來回應。

問題記錄

:::warning
這邊的問題都是根據我收集在notebookllm中的資源回應所得,並不保證是正確的
:::

  • 第一次的迭代完成之後,第二次執行/speckit.specify並沒有同步建立002-xxx分支,導致後續的執行皆在001-xx分支中執行,雖然發現這問題之後有嚐試設置SPECIFY_FEATURE,但是似乎沒有效果
    • 後續的處理方式是直接利用git branch建立對應的分支,然後git checkout過去該分支之後測試執行,確定相關文件的導讀都是從002-xxx

:::info
理論上在第二次執行/speckit.specify的時候會生成002-xxx的資料結構以及分支,這次的問題在於資料結構有生成,但是分支沒有生成,後續第三次執行的時候要特別確認。
:::

  • 001-xxx執行完畢之後,如果要第二次迭代執行/speckit.specify,是否需要切回分支master
    • 建議是需要切回再做執行,並且記得要做合併分支

:::info

  • 猜測第一次就是因為沒有切回master才會造成分支沒有成功切換為002-xxx

  • 確認即使切回master再執行仍然不會生成分支,所以這部份必需要自己建立分支之後自己切換,不過不確定是不是kilo code才有這問題,後續可以再測試github copilot

  • 調整為github copilot之後確認,使用copilot會自動切新分支,也許在kilo code上要有更明確的提示詞才會執行
    :::

  • 第2次之後執行/speckit.specify,即使技術棧沒有調整仍然需要執行/speckit.plan

  • /speckit.implement之後如果發現與task.mdplan.md上所述不同,那就要再重新執行一次/speckit.implement,並於提示詞中明確說明

  • 目前的迭代為 003-xxx,測試於specs中加入 004-xxx 的空資料夾,然後再執行/speckit.specify,系統會判定目前的執行版本為004,然後從005開始

    • 雖然很廢話,不過我還是很好奇的測試了一下就是,即使我在提示詞中很直接的說明目前為004-xxx,系統仍然是從005-來建置

開發工具從kilo code轉copilot

uvx --from git+https://github.com/github/spec-kit.git specify init --here --ai copilot --script ps

:::warning

  • 執行之後constitution.md也被重置,這部份要特別注意,也許是我的指令錯誤了
    :::

現有專案導入speckit

:::info
下面給出的是notebookllm的說明,後續再用現有的專案來測試看看
:::

這是一個非常好的問題,Spec Kit 的設計就是為了能夠導入既有的專案(在 SDD 術語中稱為 Brownfield 專案,即「遺留系統現代化」或「疊代增強」)。

將 Spec Kit 導入現有專案的關鍵在於使用正確的初始化命令,並確保您為 AI Agent 提供了足夠的專案上下文(Context)原則(Constitution),讓它能理解既有程式碼庫的結構和約束。

以下是導入 Spec Kit 到現有專案的詳細步驟和注意事項:


步驟一:導入 Spec Kit 套件

Spec Kit 提供兩種主要方式在現有專案中初始化其骨架:

方法一:使用 Specify CLI 導入(推薦)

您需要使用 specify init 命令並加上 --here 旗標,這會指示 CLI 在當前目錄下初始化 Spec Kit 必要的檔案和資料夾,而不是創建一個新的專案資料夾。

  1. 安裝 Specify CLI:如果您尚未安裝 uvx,請先安裝。

  2. 在現有專案根目錄執行

    uvx --from git+https://github.com/github/spec-kit.git specify init --here
    # 或使用句點:
    # uvx --from git+https://github.com/github/spec-kit.git specify init .
    

    您將被提示選擇要使用的 AI Agent(例如 Claude Code、GitHub Copilot 等)和腳本類型(shps)。

方法二:手動下載與複製檔案

您可以直接從 Spec Kit 的 GitHub 儲存庫的 Release 頁面 下載針對特定 Agent 的模板壓縮包,然後手動解壓縮並將相關檔案放入您的專案根目錄。

  • 複製內容:將包含 Agent 指令(例如 .github 提示或 .claude 提示)的資料夾和 .specify 資料夾(包含記憶體、腳本和模板)拖曳到您的專案根目錄中。

步驟二:建立專案憲章(Constitution)

這是導入現有專案中最關鍵的一步。由於 AI Agent 擅長模式補全,但不擅長讀心術,它需要明確的最高原則來理解現有專案的技術棧和架構。

您必須使用 /speckit.constitution 命令,來定義這些**不可協商(non-negotiable)**的原則。

  • 用途:憲章(Constitution)會將 AI 模型鎖定在現有的技術棧、代碼品質標準或架構決策上。
  • 內容範例:由於是現有專案,憲章應包含現有技術的約束,例如:「網站使用 Hugo 建構,不使用其他前端框架」、「所有 Web App 必須使用 Next.js 和 Postgress 資料庫」。
  • Agent 參考:Agent 在規劃 (/plan) 階段會參考此憲章,以確保其技術計畫符合您已設定的約定。

步驟三:提供既有程式碼庫的上下文(Context Engineering)

您必須主動確保 Agent 能夠獲取足夠的上下文,以便在不破壞現有程式碼的情況下新增功能。

  1. Agent 指令文件:許多 Agent(如 Copilot CLI)會讀取 agents.md 或類似的 Agent 檔案,您可以將專案的結構、元件定義和操作方式編碼到這些文件中,以便 LLM 理解組件關係和應在哪裡進行變更。
  2. 動態上下文分析:現代 LLM 具備動態解析程式碼樹的能力,但提供主動上下文(Proactive Context)通常效果更好。Agent 在後續的 /speckit.plan 步驟中,也會嘗試解析專案結構(例如尋找 Hugo 配置文件或 Next.js 的檔案結構),並將其應用於新功能的開發。

步驟四:新增功能 (Iterative Enhancement)

一旦基礎設置完成,您就可以像對待全新(Greenfield)專案一樣,開始為現有專案新增功能。Spec Kit 在處理現有程式碼庫的功能新增時,展現了強大的威力。

  1. 隔離開發:當您使用 /speckit.specify 命令時,Spec Kit 會自動在您的現有 Git 儲存庫中建立一個新的功能分支,將新功能的工作隔離起來,防止影響主分支的穩定性。
  2. 規格化新增功能:針對您要新增的特定功能,運行標準的 SDD 流程:
    • /speckit.specify:定義要新增什麼 (What) 和為什麼 (Why),專注於新功能如何與現有系統交互,不包含技術細節。
    • /speckit.clarify(可選):幫助您發現關於新功能要求的盲點或定義不足(underspecification),尤其在複雜的既有程式碼庫中非常有用。
    • /speckit.plan:定義如何實作 (How), Agent 會參考您在憲章中定義的既有技術棧(例如「使用 Next.js」),並根據新功能生成專屬的技術工件(如新的 API 介面或資料模型)。
    • /speckit.tasks:將計畫分解為可執行的任務,然後使用 /speckit.implement 進行實作。

總結:導入 Spec Kit 到現有專案的流程就是利用 specify init --here 進行初始化,然後利用 /speckit.constitution約束 AI Agent,使其遵循您現有專案的技術規則,最後透過結構化的 SDD 流程來疊代新增功能

相關資源

2025年4月8日 星期二

MSC 地中海郵輪與名勝世界郵輪

MSC 地中海郵輪與名勝世界郵輪

MSC 地中海郵輪與名勝世界郵輪

前言

說起我的郵輪初體驗,也就是我的「處女航」,就獻給了 MSC。而且非常「幸運」地,首航即遇上引擎故障——沒錯,我就是搭上去年那班讓不少人(可能吧?)有點羨慕的「引擎故障升等體驗」航班。必須說,雖然行程受影響,但 MSC 後續的處理確實展現了大企業該有的氣度:該趟航程直接免費,還給了一個相當大方的未來航次抵用金 (FCC),大概是你這次花五萬,下次就直接折你兩萬五的概念。也因為這樣,讓我對他們處理危機的方式留下不錯的印象,所以年底的 MSC 航程,我還是毫不猶豫地報名下去了。

至於最近的名勝世界郵輪體驗,則要感謝我老婆的妹妹熱情邀約。考量到整體花費不高,需要的請假天數也不多,回來後甚至還有兩天緩衝休息,就決定一起出門體驗看看。我們搭上的是「賞櫻首航」,非常幸運地正好遇上櫻花滿開,尤其到了岸上觀光的立山公園,那盛開的美景真的讓人心曠神怡,非常享受。後來聽說我們下一班(4月6日出發)的旅客更幸運,因為人很少(好像才九百多人),船公司更是龍蝦、生魚片大方供應,聽起來也是夠大氣!

這兩趟郵輪搭下來,從吃的、住的、玩的到服務,真的是各有千秋,有讓人想豎起大拇指按讚的地方,當然也有讓人默默搖頭的小細節。所以,這篇文章的重點,就是要來比較一下 MSC vs. 名勝世界郵輪。

在開始之前,請容我強調一下:這個比較非常主觀,沒有絕對的客觀,因為這完全是基於我個人的真實感受。你可以不同意我的看法,畢竟每個人的標準和在意的點都不同,但這就是我最直接的體驗分享。

接下來,我會用表格的方式,盡量把我還記得住的各個項目,做一個詳細的比較。如果硬要先給個簡單結論,我會說:名勝世界郵輪的 CP 值真的高到破表,而且「中文優先」的環境非常加分;但我個人實在無法接受它五樓公共區域那瀰漫的菸味。

那麼,就讓我們開始這次的比較吧!

比較總表

比較面向 MSC 地中海郵輪 名勝世界郵輪 (Resorts World Cruises) 你的觀察/備註
整體印象/氛圍 奢華感、歐式風情、國際化 親民、亞洲熟悉感、"中文優先" RWC 可能更貼近台灣旅客習慣
價格定位/CP值 感覺較高價 CP值高、價格相對親民 RWC CP值勝出
語言服務 國際語言為主 "中文優先" (服務人員、廣播) RWC 對中文旅客極友善
自助餐 品項多、變化少、口味偏西式 品項相對少、每日變化、口味合台灣胃 RWC 口味/變化勝,MSC 品項多
主餐廳 傳聞出餐慢、菜色與自助餐相似? 可能有龍蝦夜 早餐與自助餐有區隔 (佳) RWC 主餐廳感覺較用心?
飲水提供 付費瓶裝水 / 自助餐水 (口感差) 免費飲水機 (水有味需過濾) RWC 提供免費飲水是優點
娛樂活動 強調視覺 (天幕/名店街),互動性可能較弱 大廳活動多 (電影/遊戲/參與度高),感覺更吸引人 RWC 活動感覺更親民、互動性高
賭場 感覺是重點設施,吸引不少客人 (視讀者是否在意賭場)
客艙 (內艙) 基本體驗差異不大 基本體驗差異不大 (平手)
服務品質 主觀感覺較佳 中文溝通便利 MSC 服務感受好,RWC 語言無礙
兒童設施 有<2歲空間 (玩具舊)、有室內泳池 僅2-12歲空間,無室內泳池 MSC 對帶小小孩 & 壞天氣時較友善
游泳池 有室內泳池 無室內泳池 MSC 勝
吸菸政策/執行 (未遇問題) 嚴重缺點: 5樓室內吸菸嚴重,氣味擴散,管理待加強 RWC 的重大負面問題 (MSC 在此項因無負面描述而勝出)
岸上觀光 通常不包含 優點:包含免費岸上觀光 RWC 包含岸上觀光是優勢
WiFi 日計費 日計費 (平手 - 計費方式類似)
下船效率 排超久 可能因人較少排隊較快? (主觀感受) (RWC 略佔優勢?)

優缺點總結

附帶一提,在MSC的所有廣播都是先英文、日文、韓文,最後才是中文;在名勝世界的時候絕對都是先中文再英文,那種感覺是完全不一樣的。

MSC 地中海郵輪

優點:
  • 歐式奢華體驗
  • 服務品質感受佳
  • 有室內泳池
  • 有<2歲兒童空間
缺點:
  • 價格可能較高
  • 餐飲口味/變化較不合台灣胃
  • 飲水不便
  • 中文服務可能較少 (廣播順序可見一斑)

名勝世界郵輪 (Resorts World Cruises)

優點:
  • CP值高
  • "中文優先"環境
  • 餐飲口味/變化適合台灣人
  • 免費飲水機
  • 含岸上觀光
  • 活動互動性高
缺點:
  • 室內吸菸問題嚴重
  • 無室內泳池
  • 無<2歲兒童空間

給讀者的選擇建議

推薦「名勝世界郵輪」給:

  • 追求高 CP 值、預算考量者。
  • 重視中文溝通便利性。
  • 偏好符合亞洲/台灣口味餐飲。
  • 希望行程包含岸上觀光。
  • 能接受(或本身是)吸菸環境的旅客

推薦「MSC 地中海郵輪」給:

  • 偏好歐式氛圍與較高服務質感。
  • 需要室內泳池或帶<2歲幼兒同行。
  • 對室內菸味零容忍的旅客
  • 對餐飲口味接受度較廣,或願意嘗試付費餐廳。

2025年3月4日 星期二

Dify + Whisper Asr Webservice

Dify + Whisper Asr Webservice

這邊假設你已經成功啟動 dify 的服務

Whisper Asr Webservice

Whisper Asr Webservice 是一個很方便的工具,因為作者已經幫忙把語音轉文字的服務 API 化,對於應用端來說只要做常規的 API 呼叫就可以滿足應用上的需求。

啟動 Whisper Asr Webservice

啟動服務最快的方法就是使用 Docker:

docker run -d -p 9000:9000 -e ASR_MODEL=base -e ASR_ENGINE=openai_whisper \
onerahmet/openai-whisper-asr-webservice:latest
docker run -d --gpus all -p 9000:9000 -e ASR_MODEL=base -e ASR_ENGINE=openai_whisper \
onerahmet/openai-whisper-asr-webservice:latest-gpu

不過要注意自己的 CUDA 版本是否有對應,如果最新版的無法啟動的話就要降版,這取決於你的硬體設備。

OpenAPI 設置

成功啟動 docker container 之後,可以直接連接到 http://你的ip:9000/docs ,沒意外的話你會看到下圖:

openapi image

點擊紅框處的 openapi.json,系統會開啟一個新的視窗顯示一堆 JSON 格式的字串,複製出來後進行以下處理:

  1. infopaths 中間加入 servers
  2. 修正 requestBody 中的 content-type,改為 multipart/form-data; boundary=----WebKitFormBoundarydzemBAPhdeDfTCfR
更多關於模型的選擇與細部配置請參考 Whisper Asr Webservice 官方文件說明。

Dify - 自定義工具設置

進入 Dify 後,點擊「工具」→「建立自定義工具」,貼上剛剛修改的 OpenAPI json 即可建立成功。

custom tool

Dify - 應用流程開發

現在可以在 workflow 中取用自定義的語音轉文字工具:

workflow

設定參數:

params

測試應用:

test

將文字結果轉成問答集:

問題:客戶在聯絡退稅服務時需要提供哪些信息? 答案:...(省略部分內容)... 問題:退稅作業需要多久時間? 答案:退稅大約需要一個星期。 ...(更多問答內容)...

若有興趣,可以將問答轉為主持人與來賓對話,發展成 Podcast 節目;若是公司會議紀錄,也能每日播放當作內部溝通素材。

2025年1月2日 星期四

告別2024~迎接2025

告別2024~迎接2025

2024年總計讀了八篇的論文:

  1. Unpaired Image-to-Image Translation using Cycle-Consistent Adversarial Networks(CycleGAN)
  2. ArcFace: Additive Angular Margin Loss for Deep Face Recognition(翻譯)
  3. Perceptual Losses for Real-Time Style Transfer and Super-Resolution(翻譯)
  4. Image-to-Image Translation with Conditional Adversarial Networks(翻譯)
  5. Analyzing and Improving the Image Quality of StyleGAN
  6. Instance Normalization: The Missing Ingredient for Fast Stylization
  7. Distilling the knowledge in a neural network
  8. Attention Is All You Need
    以及三個系列課程:
  9. Applied LLMs Mastery 2024
  10. 李宏毅_生成式導論 2024
  11. Mathematics for Machine Learning and Data Science

不過在git上的實作記錄就偏少了,這是2025年必需要改進的部份。

當然除了讀書之外在公司內也帶著同仁一起做了幾個案子,也確認自己的方向是正確的,人工智慧對於企業有絕對的幫助。 所謂的學習是在學習一種模式(pattern),企業的資料是不斷重覆的生成,一定也存在著某種的模式可以學習。

沒有達成的部份就是Rust、Vue、Pytorch都沒有動靜,這也是要檢討的部份。

所以,我要再立下2025年新的目標:

  1. 讀10篇論文(將yolov5~v11的論文補完,擴散模型、llm相關關鍵議題論文)
  2. 三個系列課程
  3. 放棄Rust,改學C++(參考C++ Primer Plus自學)
  4. Vue3
  5. 重新復習Hello演算法中的所有演算法
  6. 公司內成功建置人臉辨識考勤系統
  7. 讀完Build a Large Language Model (From Scratch),並且自己手刻一發
  8. 熟悉幾個llm框架(dify)
  9. 補完強化學習聖經第二部中文翻譯
    滾動式增加,要讓2025更充實。

2024年4月9日 星期二

Oracle的預儲程序參數與資料表名稱相同時的異常

Oracle的預儲程序參數與資料表名稱相同時的異常

最近踩了一個坑,主要是一個預儲程序利用給定的參數來做查詢,再將查詢到的資料寫入變數,原本都還很正常,但是突然失效,偏偏單獨的查詢都很正常,遍尋不著,後來同事協助之後終於脫坑。

create or replace procedure test_proc(p_a varchar2) is
begin
	select column into v_column from table where column_a = p_a;	
end

上面是一個示意的範例,test_proc這個預儲程序在得到p_a之後就會去資料表查詢,然後將資料寫入v_column,但不知道什麼時候,資料表中被加入一個同名欄位,也就是p_a,這導致了參數中的p_a就失效了。

這可能就代表著在oracle的預儲程序中,整個上下文還是會以查詢資料表為主,查詢資料表沒有對應欄位名稱的時候才會以變數做為條件?

這很頭痛的是,這是無聲異常,而且還可能造成大量異常,不得不防,以後預儲函數的參數必需要設置的非常怪異,才不會有機會跟未來人命名欄位名稱相同。

2024年3月26日 星期二

Roland F701開箱

ROLAND F701(白色)電鋼琴

自從搬出老家之後,喵小姐一直心心唸唸著想要一台電鋼琴,終於在今年的年初,也就是過年的時候,趁著活動帶著她去一趟竹北鴻韻,雖然這已經是第n次去了。

原本預算是20k,打算買台基本的能在家過過乾癮就好,不過實際到現場按壓幾次之後,喵小姐還是決定把預算突破天際,就買了F701,常言道,捏一下就上去了,這一捏簡直是蛋破小鳥死。但是為了喵小姐的夢想我還是捏上去了。

其實最一開始是希望老泡先生可以學鋼琴,但是他從小就愛敲敲打打,所以最後選擇學習爵士鼓。現在多了一個安咕咕先生,希望他會愛上鋼琴。

購買的時候因為是要安置在三樓,多了樓層費,來的師父在安裝上也非常熟門熟路的,很辛苦,到我家已經是晚上八點多,離開的時候說他還有另外兩家要送,看來在台灣熱愛學習音樂的人真的不少。

F701在DEMO的時候有特別提到可以搭配APP,算是蠻方便的,壓下去的聲音也會隨著手指的力度而有所不同,這是當初喵小姐捨棄初階機的最主要原因。搭配耳機就可以在不擾人清夢的情況下好好練習。而且架子本身也提供一個吊掛耳機的地方,非常方便。

總之,好機,白色更是美的西哩花拉的,搭配活動送了一個升降椅,值得入手。



2024年2月27日 星期二

利用tailwind css設計一個簡易側導覽

最近在嚐試利用前端搭配flask寫網頁,幾個小功能,但是需要一個連結給使用者,所以就想要做一個側導覽功能,初始結構是20美元小助理給的,自己微調一下,符合使用比較重要,大致就是拆成html跟javascript,css的部份直接採用tailwind套版,直觀又快速。

首先是html,版面的部份要切出左側一個區域來給側導覽,然後一個按鈕,側導覽的初始狀態是整個隱藏,經過按鈕點擊來做顯示與隱藏的切換:

<div id="sidebar" class="fixed top-0 left-0 h-full bg-gray-800 transition-transform duration-300 ease-in-out -translate-x-full z-10">  
 <div class="mt-16">  
 <ul class="text-white">  
 <li class="p-4 hover:bg-gray-700"><a href="{{ url_for('Router1') }}">Router1</a></li>  
 <li class="p-4 hover:bg-gray-700"><a href="{{ url_for('Router2') }}">Router2</a></li>  
 <li class="p-4 hover:bg-gray-700"><a href="{{ url_for('Router3') }}">Router3</a></li>  
 <li class="p-4 hover:bg-gray-700"><a href="{{ url_for('Router4') }}">Router4</a></li>  
 </ul> </div></div>  
<button id="toggleNav" class="fixed top-0 left-0 mt-10 p-2 text-white bg-blue-500 rounded z-20 transition-transform duration-300 ease-in-out">  
  MenuBar  
</button>

接著在按扭地方加入一個監聽器,主要是偵測到點擊的時候就做sidebar屬性-translate-x-full的切換,然後按鈕的屬性再隨著sidebar的屬性做變化的判斷

document.addEventListener('DOMContentLoaded', function() {  
    const toggleNavButton = document.getElementById('toggleNav');  
  toggleNavButton.addEventListener('click', function() {  
        const sidebar = document.getElementById('sidebar');  
  sidebar.classList.toggle('-translate-x-full');  
  
 if(sidebar.classList.contains('-translate-x-full')){  
            toggleNavButton.style.transform = 'translateX(0px)';  
  } else {  
            toggleNavButton.style.transform = `translateX(${sidebar.offsetWidth}px)`;  
  }  
    });  
});

2024年1月30日 星期二

Graph disconnected: cannot obtain value for tensor KerasTensor

Graph disconnected: cannot obtain value for tensor KerasTensor

TensorFlow版本:2.4.0

最近在嚐試寫PGGAN,參考來源程式碼在過程中做了轉型:

input_img = tf.keras.layers.Input(shape=(4, 4, 3))
input_img = tf.cast(input_img, tf.float32)
x1 = tf.keras.layers.AveragePooling2D()(input_img)

這種情況下每次都會跳出異常訊息:

Graph disconnected: cannot obtain value for tensor KerasTensor

搞了三天三夜,不管怎麼改變寫法,改成lambda、或是tf.dtypes.cast都不行:

input_img = tf.dtypes.cast(input_img, tf.float32)

結果我就很生氣的把變數名稱改了…

input_img = tf.keras.layers.Input(shape=(4, 4, 3))
input_img2 = tf.cast(input_img, tf.float32)
x1 = tf.keras.layers.AveragePooling2D()(input_img2)

就好了,對,沒錯,就好了。恭喜我自己,怪了,那他是怎麼訓練完成的。

2024年1月25日 星期四

PyGWalker

Python Tool: PyGWalker

python pygwalker data visualization

機器學習的一個重要過程就是資料分析,在做資料分析的時候最需要的就是一個方便的資料可視化軟體,AI總是需要有BI的伴隨。BI工具著名的有QLIK、Tableau、PowerBI,如果是Python的可視化套件就更是百家爭鳴。

不過最近看到一個讓我驚豔的套件,那就是PyGWalker,簡單幾段程式就讓我擁有類似於Tableau的界面,這讓我在資料分析上多了不少便利性。

官方Github連結

安裝

這次的測試是在conda,所以記得先建立一個虛擬環境:

conda create --name PyGWalkers

確定一下環境有沒有建立:

conda env list

然後轉入虛擬環境:

activate PyGWalkers

安裝:

conda install -c conda-forge pygwalker

安裝ipykernel:

conda install ipykernel

註冊kernel(要記得註冊,不然在執行中就無法選擇對應的kernel):

python -m ipykernel install --user --name PyGWalkers --display-name "Python (PyGWalkers)"

安裝確認:

conda list

啟動:

jupyter notebook

實作測試

從官方範例來看,就是簡單的兩步:

  1. 載入資料(csv -> dataframe)
  2. 執行
# 載入需求套件
import pandas as pd
import pygwalker as pyg
# 指定資料來源路徑
file_path = r'your file path'
# 載入dataframe
df = pd.read_csv(file_path)
# 確認資料是否確實載入
df.head()
# johnnie walker
pyg.walk(df)

image for PyGWalker

執行之後就會進入一個互動式的UI介面,是不是很讓人驚豔?剩下的就只是拖拖拉拉來觀察資料之間的關聯性了,這對小眾資料處理人員來說是非常便利的工具。

2023年8月23日 星期三

Python + SAPGUI Script = 自動化重覆性事務性作業

Python + SAPGUI Script = 自動化重覆性事務性作業

reference

今天因為其他部門的資訊作業上的問題導致我這邊必需協助刪除庫存批屬性,雖然才八十幾筆,但也是讓我感到憤怒,憤怒可以提升技術力,所以我決定找方法自動化處理。

兩年前的今天,k-weiming寫下這個,真的是緣份,我的環境是python 3.11,需要的工具就是pywin32:

pip install pywin32

sap的設置也要確認,不過這取決於公司的管理策略,如果剛好沒被管控的話那就可以使用:

  1. Enable scripting:啟用scripting
  2. Notify when a script attaches to SAP GUI:對接上的時候是不是會跳出訊息,另一個也是一樣的設置

準備工作完成,再來就是要先取得執行的GUI的session:

import win32com.client def get_client(): #: 從com服務器中取得SAPGUI的物件 sap_gui_auto = win32com.client.GetObject('SAPGUI') if not type(sap_gui_auto) == win32com.client.CDispatch: print("SAPGUI is not running") return #: 取得SAPGUI的腳本引擎,所以接上的時候GUI會出現警示訊息 #: 這可以從gui設罝上取消 application = sap_gui_auto.GetScriptingEngine if not type(application) == win32com.client.CDispatch: print("SAPGUI is not running") return #: 這邊取得的是實際啟動的SAPGUI的物件 #: 以實際登入的使用者為條件 for conn in range(application.Children.Count): connection = application.Children(conn) #: 這邊取得的是登入的帳號所開啟的視窗(session) for sess in range(connection.Children.Count): session = connection.Children(sess) if session.Info.Transaction == "SESSION_MANAGER": return session

接下來的動作我們可以先利用錄製的方式來取得腳本,畢竟欄位框那麼多,即使看著官方的文件也很難一個一個找出來,最好的方法就是讓系統幫我們找出來。

開啟錄製程式:

錄製程式開啟之後如下:

中間那個圓點就是腳本錄製,腳本的保存地方則是根據你的『Save To』而定。

錄製完成之後在指定資料夾就會有vbstxt檔,打開txt檔,裡面會有你的腳本資訊:

紅框處的部份就是操作過程的相關物件的定位資訊以及操作的動作,像是按下去或是選擇某個分頁之類的。

以我自己的作業目標為例,我是想要自動化刪除很多庫存批屬性資料,所以就可以這麼做:

if __name__ == '__main__': #: reference #: https://k-weiming.github.io/2021-08-23-sap-connection-python/ session = get_client() #: 到SAP的交易代碼輸入欄位輸入MSC2N session.findById("wnd[0]/tbar[0]/okcd").text = "/nMSC2N" #: 按下Enter session.findById("wnd[0]").sendVKey(0) #: 輸入物料編號 session.findById("wnd[0]/usr/subSUBSCR_BATCH_MASTER:SAPLCHRG:1111/subSUBSCR_HEADER:SAPLCHRG:1500/ctxtDFBATCH-MATNR").text = "CA09B2002-1Z01" #: 輸入工廠 session.findById("wnd[0]/usr/subSUBSCR_BATCH_MASTER:SAPLCHRG:1111/subSUBSCR_HEADER:SAPLCHRG:1500/ctxtDFBATCH-WERKS").text = "A101" #: 輸入批號 session.findById("wnd[0]/usr/subSUBSCR_BATCH_MASTER:SAPLCHRG:1111/subSUBSCR_HEADER:SAPLCHRG:1500/ctxtDFBATCH-CHARG").text = "A120069722" #: 按下Enter session.findById("wnd[0]").sendVKey(0) #: 選擇批屬性分頁 session.findById("wnd[0]/usr/subSUBSCR_BATCH_MASTER:SAPLCHRG:1111/subSUBSCR_TABSTRIP:SAPLCHRG:2000/tabsTS_BODY/tabpCLAS").select() #: 按下刪除 session.findById("wnd[0]/usr/subSUBSCR_BATCH_MASTER:SAPLCHRG:1111/subSUBSCR_TABSTRIP:SAPLCHRG:2000/tabsTS_BODY/tabpCLAS/ssubSUBSCR_BODY:SAPLCHRG:2300/btnCLASS_DEL").press()

我的範例中少了按下保存的動作,因為只是測試,所以就沒有錄保存的動作。

這接口很方便,因為我不需要去寫abap來做錄製,不過這種作法應該是比較適用於重覆性的事務性作業,又或者剛好沒有BAPI、RFC可以使用,那就可以用這種方式來搜集資料。

2023年4月14日 星期五

新冠感染記實

最近我也終於感染新冠,在病毒的挑選之下,我真正成為一個新人類,我是約翰康納,如果你聽到....不是,我是說,我記錄一下我的狀況:

1. 發燒來的又快又急,晚上洗好澡之後就瞬間牙起來,體溫馬上升到38.2

2. 體溫開始無限制向上來到39.7

3. 全身發寒,發熱,大概跟張無忌一樣的情況,會冷到不自覺的發抖

4. 因為吃普拿疼,晚上睡覺大量出汗,床單跟棉被都溼

上面這個部份是四天內的循環,基本上吃了普拿疼之後的兩小時藥效生效之後就會降到37.9~38.5的溫度。我在第三天的時候喉嚨痛的症頭出現,連吞口水都覺得生不如死,所以喝楊桃汁跟吃喉糖,大概第五天之後喉嚨痛的症頭就消失了。

每天洗澡的時候我會讓自己用力一咳,因為一整天的痰都不知不覺得卡在喉嚨深處,只要你咳就會發現有一口濃濃的痰,超級噁心,一定要咳掉。

偶爾會出現鼻塞的情況,但不嚴重,然後咳的頻率還好,基本還可以忍住,喝了五天的清冠一號,水煎藥,甘甜可口,入口回甘。

不過最糟的是我發現我的味覺出現嚴重問題,我一直覺得鼻子的某個地方好像被什麼東西覆蓋住,導致我的味覺中有一個關鍵味道嚐不出來,這造成我喝咖啡變的超級噁心。

短短的五天基本上對我來說非常漫長,一人中獎多人累攤,還要擔心同住家人也受感染。只是個人生記錄,希望不要再有下次了,累。

2023年2月20日 星期一

Flask 2.x實作開工

hackmd的book連結

在職修課期間有幾位網友問我會不會針對2.x的部份再寫些新的東西,當初真的是心有餘而力不足,現在其實也還是,因為我在玩天堂w...

不過如何總算是開始動手了,除了重寫所用到的extension之外,題目的部份也暫定是天堂w的血盟分鑽管理,如果這個題目走不下去的話就會調整成自己寫一個記帳網頁。

會選擇血盟分鑽的題目是因為,有在玩的就知道,每一筆交易都需要手續費,過於頻繁的交易會付出過多的手續費。如果可以月結的話,那也許無形成也可以省下一筆不少的費用,因為角色之間可以互相沖帳。

當然如果真的不行的話變成是一個記帳網頁也不錯,生活中很多開銷如果可以好好記錄,回頭可以看到不少亮點。雖然現在也已經有很多的手機app可以使用,而且有更好的服務,但是寫一個專屬於自己的工具那種感覺是很不錯的。

最重要的當然就是我們可以從實作中瞭解到Flask帶給我們的便利性,然後從一個lib延伸學習到更多的lib。舉例來說,flask就單純的是flask,你想要跟資料庫對接也許就會考慮使用flask-sqlalchemy,使用flask-sqlalchemy也許你會採用orm,那你就學到orm於sqlalchemy的整合應用。

總之,還是那句話,flask是自由的,自由的意思就是很多事情你要自己處理,而不是由flask這個框架幫你決定一切。

PS: 目前已重置完成flask-sqlalchemy、flask-login


2023年2月18日 星期六

YOLOv3實作

YOLOv3實作整理

tags: YOLOv3 YOLO DeepLearning

相關模型可參考在下的github

YOLOv3,現在都已經v8了,我可能才剛整理好v3,因為我個人習慣上是盡可能的希望可以自己硬寫一發,神經網路可能是黑盒子無法解釋,但是方法本身如果是可解釋的我就盡可能的去瞭解,不過還是有一些小細節沒有很完整,但至少我有信心看完我寫的資料之後你也可以絕大部份的掌握YOLOv3,這對你再往後延伸閱讀很有幫助。

物體偵測的資料格式與一般分類任務的標記有很大的不同,也因此我們在實作之前一定要先能夠處理資料的轉變。舉例來說,如果是分類任務的話,那一張照片也許就是一個類別,但如果是物體偵測的話,那也許一張照片上面就有四五個物體標記資訊了。

也因此,我們就先需要做些資料處理的前置動作,這部份你可以參考Arch_YOLO_dataset_preprocess_1.ipynb以及Arch_YOLO_dataset_preprocess_2_data_for_yolov3.ipynb

這兩份文件主要在說明資料的前置處理,以及產生模型中計算loss所需要的資料格式。

整理好訓練資料之後就可以開始著手處理模型,我們知道,YOLOv3採用Darknet-53做為骨幹,這意謂著它有53層,骨幹的意思就是最主要的模型架構,但這是骨幹,YOLOv3的輸出跟Darknet-53是不一樣的,一個是做為物體偵測,一個是做為分類任務。

下圖給出YOLOv3的完整架構:

image alt

可以看的出來主要是將不同大小的feature map做結合,讓大小資訊可以結合在一起做為輸出預測,這樣子不同尺寸的ancbor box就可以有比較豐富的資訊。

針對Darknet與YOLOv3的模型說明可以參考Arch_YOLOv3_1_Darknet-53_structure.ipynbArch_YOLOv3_2_YOLOv3_structure.ipynb的說明。這兩篇文章中對於架構的部份有著較為詳細的說明,可以更直觀的瞭解到模型中特徵圖(feature map)的變化。

接下來要處理的就是loss function,自己手寫過一次就會有一種,『啊,原來還可以這樣處理』的感覺出現。如下圖所示:

loss function的部份大致分為三個部份:

  1. 框的好不好
  2. 有沒有信心
  3. 框到什麼

詳細的細節可以參考Arch_YOLOv3_3_loss_function。框的好不好可以用IoU來判斷,有沒有信心跟框到什麼就是用cross_entropy,這很標準。不過或許是因為我沒有先用影像分類的任務來做預訓練,訓練起來有點不順利,也因此loss function的部份我在實作上是有做些許調整的。

Arch_YOLOv3_3_loss_function裡面有一個函數,decoder,這主要是拿來調整模型得到的feature map,整個資料流大致如下:

現在我們已經可以進入訓練的步驟了,有一些說不上來的細節,像是AP50之類的計算我還沒有實作上,所以目前來看是沒有這一個數值可以提供參考的,唯一有的就是loss。正如上面提到的,或許是因為我沒有先做預訓練,所以訓練過程中常常會造成梯度的消失或是爆炸等問題。也因此整個訓練過程真的很長,常常早上看的時候發現loss又nan,我把我的訓練過程通通寫在Arch_YOLOv3_4_training,算是幫自己做個過程中記錄:

訓練之後我們需要做個推論測試,所以拿了張照片就看看它的原始狀態,再用我們訓練完成的模型看看推論出來的狀態,這可以參考Arch_YOLOv3_5_predict。得到的推論結果如下:

上圖左是原始框的資料,然後上圖右是訓練後模型推論得到的資料,結果看似不錯,不過中間還是存在置信度的問題就是了,或許還需要更次的迭代。

以上就差不多是整個模型的處理過程了,前陣子已經看到發表YOLOv8了,我的v4才正要開始動手,有點慢,不過相信有這個基礎在可以很快速的推進才是。