2025年6月29日 星期日

2025 年第 26 周記事

終於來到第 26 周, 也就是 2025 過一半啦! 我離退休也剩 6 年了, 每天不斷學習, 不知老之將至啊!




最近手殘無端搞出兩個問題, 上週六回到鄉下趁著車沒熄火連上 WiFi 打算更新導航機圖資, 沒想到彈出一個視窗說有新版軟體是否要更新, 想說順便升版吧! 按 YES 安裝後重啟出現要求輸入授權碼頁面, 去年裝機時老闆沒給啊? 趕緊打給老闆問怎麼辦, 結果說要拆機看後面的貼紙才知道, 所以週二下班把車開過去, 但主機拉出來卻沒看到授權碼, 只好跟 Papago 工程師聯絡後才搞定. 第二件三八的則是我看到 Mapleboard 彈出有新版軟體問是否要更新, 同樣地, 更新完我的網站主機連不上了, 原來新版多了 Guest 帳戶無連線權限, 所以就斷網了, 這個周末也花了一些時間才讓主機恢復運轉. 教訓是 : 系統不要亂更新, 可能會帶來麻煩. 

本周終於把 Disney+ 的日劇 "噬亡村" 看完了 (第一, 二季), 原來後藤家的男人在後藤銀與樫羽部結盟攻滅後藤家時就死絕了, 岩男這些自稱後藤家的人其實都是食人的樫羽部族人. 此劇改編自漫畫我覺得真的很好看. 

本周學習重點回到 OpenAI, 已經測試完 Image API, 但不時又回去增補 Gradio 與 Streamlit 的筆記. 要學的東西很多, 只能一步一步慢慢拆解, 下周打算開始在 MSI 桌機上安裝 Ollama. 

Mapleboard MP510-50 測試 (二十四) : 安裝 Streamlit 與 Gradio

昨晚處理完 Guest 帳戶問題後網站重新上線, 就打鐵趁熱繼續向前推進, 安裝兩個我最常用的 Python Web App 開發套件 : Streamlit 與 Gradio. 自從學會它們的用法後, 就比較少用 Django 與 Flask 開發網頁應用了.

本系列全部測試筆記索引參考 :



1. 安裝 Streamlit : 

Streamlit 是一個專為資料科學家, 分析師與開發者設計的 Python Web 應用框架, Python 程式開發者毋須熟悉 HTML / CSS / JS 等網頁相關技術即可快速建立一個資料互動, 模型測試或可視化的 Web 應用, 特別適合下列應用場景 : 
  • 資料視覺化應用
  • 機器學習模型展示與測試平台
  • 表單或工具類應用
  • 即時互動儀表板
  • 自助式資料應用系統
  • 簡易部署的 Python Web 應用
但對於下列應用則不適合 :
  • 多人同時使用的大型商業系統 (不支援使用者登入與會話管理)
  • 即時遊戲 (缺乏 Canvas 動態動畫精細控制功能)
  • 高客製化 UI 設計需求 (Streamlit 元件樣式固定)
使用 pip3 安裝 :

tony1966@LX2438:~$ pip3 install streamlit   
Defaulting to user installation because normal site-packages is not writeable
Collecting streamlit
  Downloading streamlit-1.46.1-py3-none-any.whl (10.1 MB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 10.1/10.1 MB 6.1 MB/s eta 0:00:00
Collecting tornado!=6.5.0,<7,>=6.0.3
  Downloading tornado-6.5.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (443 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 443.7/443.7 KB 7.2 MB/s eta 0:00:00
Collecting altair<6,>=4.0
  Downloading altair-5.5.0-py3-none-any.whl (731 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 731.2/731.2 KB 7.4 MB/s eta 0:00:00
Collecting cachetools<7,>=4.0
  Downloading cachetools-6.1.0-py3-none-any.whl (11 kB)
Collecting numpy<3,>=1.23
  Downloading numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (14.3 MB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 14.3/14.3 MB 5.7 MB/s eta 0:00:00
Collecting pandas<3,>=1.4.0
  Downloading pandas-2.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (11.7 MB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 11.7/11.7 MB 6.1 MB/s eta 0:00:00
Collecting gitpython!=3.1.19,<4,>=3.0.7
  Downloading GitPython-3.1.44-py3-none-any.whl (207 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 207.6/207.6 KB 6.2 MB/s eta 0:00:00
Collecting watchdog<7,>=2.1.5
  Downloading watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl (79 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 79.1/79.1 KB 4.1 MB/s eta 0:00:00
Requirement already satisfied: click<9,>=7.0 in ./.local/lib/python3.10/site-packages (from streamlit) (8.1.8)
Requirement already satisfied: toml<2,>=0.10.1 in /usr/lib/python3/dist-packages (from streamlit) (0.10.2)
Collecting protobuf<7,>=3.20
  Downloading protobuf-6.31.1-cp39-abi3-manylinux2014_aarch64.whl (322 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 322.1/322.1 KB 6.1 MB/s eta 0:00:00
Collecting pyarrow>=7.0
  Downloading pyarrow-20.0.0-cp310-cp310-manylinux_2_28_aarch64.whl (40.7 MB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 40.7/40.7 MB 3.9 MB/s eta 0:00:00
Requirement already satisfied: typing-extensions<5,>=4.4.0 in ./.local/lib/python3.10/site-packages (from streamlit) (4.12.0)
Requirement already satisfied: pillow<12,>=7.1.0 in /usr/lib/python3/dist-packages (from streamlit) (9.0.1)
Requirement already satisfied: blinker<2,>=1.5.0 in ./.local/lib/python3.10/site-packages (from streamlit) (1.9.0)
Collecting pydeck<1,>=0.8.0b4
  Downloading pydeck-0.9.1-py2.py3-none-any.whl (6.9 MB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 6.9/6.9 MB 6.4 MB/s eta 0:00:00
Collecting requests<3,>=2.27
  Downloading requests-2.32.4-py3-none-any.whl (64 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 64.8/64.8 KB 3.1 MB/s eta 0:00:00
Collecting packaging<26,>=20
  Downloading packaging-25.0-py3-none-any.whl (66 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 66.5/66.5 KB 2.6 MB/s eta 0:00:00
Collecting tenacity<10,>=8.1.0
  Downloading tenacity-9.1.2-py3-none-any.whl (28 kB)
Requirement already satisfied: jinja2 in ./.local/lib/python3.10/site-packages (from altair<6,>=4.0->streamlit) (3.1.5)
Collecting jsonschema>=3.0
  Downloading jsonschema-4.24.0-py3-none-any.whl (88 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 88.7/88.7 KB 3.6 MB/s eta 0:00:00
Collecting narwhals>=1.14.2
  Downloading narwhals-1.44.0-py3-none-any.whl (365 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 365.2/365.2 KB 5.5 MB/s eta 0:00:00
Collecting gitdb<5,>=4.0.1
  Downloading gitdb-4.0.12-py3-none-any.whl (62 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 62.8/62.8 KB 3.2 MB/s eta 0:00:00
Collecting python-dateutil>=2.8.2
  Downloading python_dateutil-2.9.0.post0-py2.py3-none-any.whl (229 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 229.9/229.9 KB 4.7 MB/s eta 0:00:00
Collecting tzdata>=2022.7
  Downloading tzdata-2025.2-py2.py3-none-any.whl (347 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 347.8/347.8 KB 6.6 MB/s eta 0:00:00
Requirement already satisfied: pytz>=2020.1 in /usr/lib/python3/dist-packages (from pandas<3,>=1.4.0->streamlit) (2022.1)
Collecting charset_normalizer<4,>=2
  Downloading charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (144 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 144.6/144.6 KB 3.2 MB/s eta 0:00:00
Requirement already satisfied: idna<4,>=2.5 in /usr/lib/python3/dist-packages (from requests<3,>=2.27->streamlit) (3.3)
Requirement already satisfied: urllib3<3,>=1.21.1 in /usr/lib/python3/dist-packages (from requests<3,>=2.27->streamlit) (1.26.5)
Requirement already satisfied: certifi>=2017.4.17 in ./.local/lib/python3.10/site-packages (from requests<3,>=2.27->streamlit) (2024.2.2)
Collecting smmap<6,>=3.0.1
  Downloading smmap-5.0.2-py3-none-any.whl (24 kB)
Requirement already satisfied: MarkupSafe>=2.0 in ./.local/lib/python3.10/site-packages (from jinja2->altair<6,>=4.0->streamlit) (3.0.2)
Collecting referencing>=0.28.4
  Downloading referencing-0.36.2-py3-none-any.whl (26 kB)
Collecting rpds-py>=0.7.1
  Downloading rpds_py-0.25.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (386 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 386.2/386.2 KB 2.9 MB/s eta 0:00:00
Collecting jsonschema-specifications>=2023.03.6
  Downloading jsonschema_specifications-2025.4.1-py3-none-any.whl (18 kB)
Requirement already satisfied: attrs>=22.2.0 in ./.local/lib/python3.10/site-packages (from jsonschema>=3.0->altair<6,>=4.0->streamlit) (23.2.0)
Requirement already satisfied: six>=1.5 in /usr/lib/python3/dist-packages (from python-dateutil>=2.8.2->pandas<3,>=1.4.0->streamlit) (1.16.0)
Installing collected packages: watchdog, tzdata, tornado, tenacity, smmap, rpds-py, python-dateutil, pyarrow, protobuf, packaging, numpy, narwhals, charset_normalizer, cachetools, requests, referencing, pydeck, pandas, gitdb, jsonschema-specifications, gitpython, jsonschema, altair, streamlit
Successfully installed altair-5.5.0 cachetools-6.1.0 charset_normalizer-3.4.2 gitdb-4.0.12 gitpython-3.1.44 jsonschema-4.24.0 jsonschema-specifications-2025.4.1 narwhals-1.44.0 numpy-2.2.6 packaging-25.0 pandas-2.3.0 protobuf-6.31.1 pyarrow-20.0.0 pydeck-0.9.1 python-dateutil-2.9.0.post0 referencing-0.36.2 requests-2.32.4 rpds-py-0.25.1 smmap-5.0.2 streamlit-1.46.1 tenacity-9.1.2 tornado-6.5.1 tzdata-2025.2 watchdog-6.0.0

檢視版本 :

Python 3.10.12 (/usr/bin/python3)  
>>> import streamlit as st   
>>> st.__version__     
'1.46.1' 


2. 安裝 Gradio : 

Gradio 是一個用來快速建立機器學習模型 Web 介面的 Python 函式庫, 它最主要的強項就是讓開發者快速把 ML 模型包成一個可測試的介面, 只需要單行程式碼 gr.Interface(fn, inputs, outputs) 就能完成基本模型包裝, 最適合的應用情境如下 : 
  • ML 模型互動測試工具
  • 互動式 AI 工具
  •  多人使用的小型應用服務
  • AI App Demo 平台(如 Hugging Face Spaces)
  • 快速原型設計
但對於下列應用則不適合 :
  • 高客製化 UI 設計需求 (Gradio 元件樣式固定)
  • 需要導覽列或多頁表單的多頁面應用 (Blocks 架構不如 Streamlit 有彈性) 
  • 商用大型平台 (Gradio 缺乏使用者登入, 資料庫整合, 與會話管理等後端功能)
  • 即時遊戲 (Gradio 缺乏動態資料視覺化功能)
使用 pip3 安裝 : 

tony1966@LX2438:~$ pip3 install gradio   
Defaulting to user installation because normal site-packages is not writeable
Collecting gradio
  Downloading gradio-5.35.0-py3-none-any.whl (54.3 MB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 54.3/54.3 MB 3.6 MB/s eta 0:00:00
Collecting huggingface-hub>=0.28.1
  Downloading huggingface_hub-0.33.1-py3-none-any.whl (515 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 515.4/515.4 KB 5.2 MB/s eta 0:00:00
Collecting typer<1.0,>=0.12
  Downloading typer-0.16.0-py3-none-any.whl (46 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 46.3/46.3 KB 2.7 MB/s eta 0:00:00
Collecting anyio<5.0,>=3.0
  Downloading anyio-4.9.0-py3-none-any.whl (100 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100.9/100.9 KB 5.5 MB/s eta 0:00:00
Collecting tomlkit<0.14.0,>=0.12.0
  Downloading tomlkit-0.13.3-py3-none-any.whl (38 kB)
Requirement already satisfied: markupsafe<4.0,>=2.0 in ./.local/lib/python3.10/site-packages (from gradio) (3.0.2)
Collecting safehttpx<0.2.0,>=0.1.6
  Downloading safehttpx-0.1.6-py3-none-any.whl (8.7 kB)
Collecting aiofiles<25.0,>=22.0
  Downloading aiofiles-24.1.0-py3-none-any.whl (15 kB)
Requirement already satisfied: pillow<12.0,>=8.0 in /usr/lib/python3/dist-packages (from gradio) (9.0.1)
Collecting ruff>=0.9.3
  Downloading ruff-0.12.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (10.7 MB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 10.7/10.7 MB 5.3 MB/s eta 0:00:00
Collecting pydub
  Downloading pydub-0.25.1-py2.py3-none-any.whl (32 kB)
Requirement already satisfied: numpy<3.0,>=1.0 in ./.local/lib/python3.10/site-packages (from gradio) (2.2.6)
Collecting fastapi<1.0,>=0.115.2
  Downloading fastapi-0.115.14-py3-none-any.whl (95 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 95.5/95.5 KB 4.4 MB/s eta 0:00:00
Collecting pydantic<2.12,>=2.0
  Downloading pydantic-2.11.7-py3-none-any.whl (444 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 444.8/444.8 KB 6.2 MB/s eta 0:00:00
Collecting orjson~=3.0
  Downloading orjson-3.10.18-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (136 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 137.0/137.0 KB 5.7 MB/s eta 0:00:00
Collecting python-multipart>=0.0.18
  Downloading python_multipart-0.0.20-py3-none-any.whl (24 kB)
Collecting gradio-client==1.10.4
  Downloading gradio_client-1.10.4-py3-none-any.whl (323 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 323.9/323.9 KB 6.8 MB/s eta 0:00:00
Collecting httpx>=0.24.1
  Downloading httpx-0.28.1-py3-none-any.whl (73 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 73.5/73.5 KB 3.9 MB/s eta 0:00:00
Collecting groovy~=0.1
  Downloading groovy-0.1.2-py3-none-any.whl (14 kB)
Requirement already satisfied: packaging in ./.local/lib/python3.10/site-packages (from gradio) (25.0)
Requirement already satisfied: jinja2<4.0 in ./.local/lib/python3.10/site-packages (from gradio) (3.1.5)
Collecting semantic-version~=2.0
  Downloading semantic_version-2.10.0-py2.py3-none-any.whl (15 kB)
Collecting uvicorn>=0.14.0
  Downloading uvicorn-0.35.0-py3-none-any.whl (66 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 66.4/66.4 KB 3.2 MB/s eta 0:00:00
Collecting ffmpy
  Downloading ffmpy-0.6.0-py3-none-any.whl (5.5 kB)
Collecting starlette<1.0,>=0.40.0
  Downloading starlette-0.47.1-py3-none-any.whl (72 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 72.7/72.7 KB 4.1 MB/s eta 0:00:00
Requirement already satisfied: pandas<3.0,>=1.0 in ./.local/lib/python3.10/site-packages (from gradio) (2.3.0)
Requirement already satisfied: pyyaml<7.0,>=5.0 in /usr/lib/python3/dist-packages (from gradio) (5.4.1)
Requirement already satisfied: typing-extensions~=4.0 in ./.local/lib/python3.10/site-packages (from gradio) (4.12.0)
Collecting websockets<16.0,>=10.0
  Downloading websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (182 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 182.3/182.3 KB 399.3 kB/s eta 0:00:00
Collecting fsspec
  Downloading fsspec-2025.5.1-py3-none-any.whl (199 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 199.1/199.1 KB 6.7 MB/s eta 0:00:00
Requirement already satisfied: idna>=2.8 in /usr/lib/python3/dist-packages (from anyio<5.0,>=3.0->gradio) (3.3)
Requirement already satisfied: sniffio>=1.1 in ./.local/lib/python3.10/site-packages (from anyio<5.0,>=3.0->gradio) (1.3.1)
Requirement already satisfied: exceptiongroup>=1.0.2 in ./.local/lib/python3.10/site-packages (from anyio<5.0,>=3.0->gradio) (1.2.1)
Collecting starlette<1.0,>=0.40.0
  Downloading starlette-0.46.2-py3-none-any.whl (72 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 72.0/72.0 KB 3.3 MB/s eta 0:00:00
Requirement already satisfied: certifi in ./.local/lib/python3.10/site-packages (from httpx>=0.24.1->gradio) (2024.2.2)
Collecting httpcore==1.*
  Downloading httpcore-1.0.9-py3-none-any.whl (78 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 78.8/78.8 KB 4.0 MB/s eta 0:00:00
Collecting h11>=0.16
  Downloading h11-0.16.0-py3-none-any.whl (37 kB)
Collecting hf-xet<2.0.0,>=1.1.2
  Downloading hf_xet-1.1.5-cp37-abi3-manylinux_2_28_aarch64.whl (3.0 MB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 3.0/3.0 MB 5.6 MB/s eta 0:00:00
Requirement already satisfied: requests in ./.local/lib/python3.10/site-packages (from huggingface-hub>=0.28.1->gradio) (2.32.4)
Collecting tqdm>=4.42.1
  Downloading tqdm-4.67.1-py3-none-any.whl (78 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 78.5/78.5 KB 4.0 MB/s eta 0:00:00
Collecting filelock
  Downloading filelock-3.18.0-py3-none-any.whl (16 kB)
Requirement already satisfied: tzdata>=2022.7 in ./.local/lib/python3.10/site-packages (from pandas<3.0,>=1.0->gradio) (2025.2)
Requirement already satisfied: python-dateutil>=2.8.2 in ./.local/lib/python3.10/site-packages (from pandas<3.0,>=1.0->gradio) (2.9.0.post0)
Requirement already satisfied: pytz>=2020.1 in /usr/lib/python3/dist-packages (from pandas<3.0,>=1.0->gradio) (2022.1)
Collecting typing-inspection>=0.4.0
  Downloading typing_inspection-0.4.1-py3-none-any.whl (14 kB)
Collecting typing-extensions~=4.0
  Downloading typing_extensions-4.14.0-py3-none-any.whl (43 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 43.8/43.8 KB 2.1 MB/s eta 0:00:00
Collecting annotated-types>=0.6.0
  Downloading annotated_types-0.7.0-py3-none-any.whl (13 kB)
Collecting pydantic-core==2.33.2
  Downloading pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (1.9 MB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.9/1.9 MB 3.1 MB/s eta 0:00:00
Collecting shellingham>=1.3.0
  Downloading shellingham-1.5.4-py2.py3-none-any.whl (9.8 kB)
Requirement already satisfied: click>=8.0.0 in ./.local/lib/python3.10/site-packages (from typer<1.0,>=0.12->gradio) (8.1.8)
Collecting rich>=10.11.0
  Downloading rich-14.0.0-py3-none-any.whl (243 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 243.2/243.2 KB 7.6 MB/s eta 0:00:00
Requirement already satisfied: six>=1.5 in /usr/lib/python3/dist-packages (from python-dateutil>=2.8.2->pandas<3.0,>=1.0->gradio) (1.16.0)
Collecting markdown-it-py>=2.2.0
  Downloading markdown_it_py-3.0.0-py3-none-any.whl (87 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 87.5/87.5 KB 4.4 MB/s eta 0:00:00
Collecting pygments<3.0.0,>=2.13.0
  Downloading pygments-2.19.2-py3-none-any.whl (1.2 MB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.2/1.2 MB 5.9 MB/s eta 0:00:00
Requirement already satisfied: urllib3<3,>=1.21.1 in /usr/lib/python3/dist-packages (from requests->huggingface-hub>=0.28.1->gradio) (1.26.5)
Requirement already satisfied: charset_normalizer<4,>=2 in ./.local/lib/python3.10/site-packages (from requests->huggingface-hub>=0.28.1->gradio) (3.4.2)
Collecting mdurl~=0.1
  Downloading mdurl-0.1.2-py3-none-any.whl (10.0 kB)
Installing collected packages: pydub, websockets, typing-extensions, tqdm, tomlkit, shellingham, semantic-version, ruff, python-multipart, pygments, orjson, mdurl, hf-xet, h11, groovy, fsspec, filelock, ffmpy, annotated-types, aiofiles, uvicorn, typing-inspection, pydantic-core, markdown-it-py, huggingface-hub, httpcore, anyio, starlette, rich, pydantic, httpx, typer, safehttpx, gradio-client, fastapi, gradio
  Attempting uninstall: typing-extensions
    Found existing installation: typing_extensions 4.12.0
    Uninstalling typing_extensions-4.12.0:
      Successfully uninstalled typing_extensions-4.12.0
  Attempting uninstall: h11
    Found existing installation: h11 0.14.0
    Uninstalling h11-0.14.0:
      Successfully uninstalled h11-0.14.0
Successfully installed aiofiles-24.1.0 annotated-types-0.7.0 anyio-4.9.0 fastapi-0.115.14 ffmpy-0.6.0 filelock-3.18.0 fsspec-2025.5.1 gradio-5.35.0 gradio-client-1.10.4 groovy-0.1.2 h11-0.16.0 hf-xet-1.1.5 httpcore-1.0.9 httpx-0.28.1 huggingface-hub-0.33.1 markdown-it-py-3.0.0 mdurl-0.1.2 orjson-3.10.18 pydantic-2.11.7 pydantic-core-2.33.2 pydub-0.25.1 pygments-2.19.2 python-multipart-0.0.20 rich-14.0.0 ruff-0.12.1 safehttpx-0.1.6 semantic-version-2.10.0 shellingham-1.5.4 starlette-0.46.2 tomlkit-0.13.3 tqdm-4.67.1 typer-0.16.0 typing-extensions-4.14.0 typing-inspection-0.4.1 uvicorn-0.35.0 websockets-15.0.1

檢視版本 : 

>>> import gradio as gr  
>>> gr.__version__  
'5.35.0'

Mapleboard MP510-50 測試 (二十三) : 關閉 Guest 帳戶

本篇主要是記錄 Mapleboard 的 Ubuntu Mate 因為升版加入了 Guest 帳戶導致主機無法連上網路問題的解決方法. 
 
本系列之前的文章參考 :


上週在鄉下看到 Mapleboard 跳出 Ubuntu 有更新, 詢問是否要下載安裝, 我沒多想按了 Yes 後就沒管它, 可能安裝完會自動重開機, 我也沒時間去看, 結果這周我在高雄想連線我的個人網站 (佈署在 Mapleboard 裡面) 居然無法連線, 但我一時沒有將其關聯到是作業系統升版更新造成, 還以為是鄉下的網路有問題. 

昨天 (週六) 回到鄉下檢視, 發現 Mapleboard 出現奇怪的 "訪客作業階段" 登入頁面 :




我問 AI 這是甚麼鬼? 它回答如下 :

"Ubuntu 出現「訪客作業階段(Guest Session)」通常是為了讓其他使用者可以暫時使用電腦、上網或進行基本操作,而不需知道主帳戶的密碼,也不會對主系統造成永久更動。"

Guest 帳號不需要密碼, 可直接進入系統但只能執行基本的操作, 這應該是升版後才有的, 之前重開機都只有預設的 one 與我新增的個人帳號 (自動用 pppoe 連線光世代網路), 這個 Guest 無此權限, 難怪網站無法運作. 

解決辦法是用下列指令編輯 lightdm.conf 設定檔, 參考 : 


以管理員身份用 nano 編輯 : 

tony1966@LX2438:~$ sudo nano /etc/lightdm/lightdm.conf   

然後添加下面資訊後重開機即可 :

    [SeatDefaults]
    allow-guest=false

由於 Guest 帳號無法使用 sudo, 所以點選上方的 one 或個人帳號輸入密碼登入後將 /etc/lightdm/lightdm.conf 改為如下內容 :

    [Seat:*]
    #autologin-user=one
    autologin-user-timeout=0
    [SeatDefaults]  
    allow-guest=false  

此處上面三列為原有的內容, 後面兩列才是新增的, 存檔後重開機就不會出現 "訪客作業階段(Guest Session)" 訊息了. 

雖然順利解決了, 但此事給我一個教訓 : 系統跑得好好的不要亂動, 新不一定好. 

2025年6月28日 星期六

Python 學習筆記 : 簡單好用的 Web app 套件 gradio (十)

最近幾天在測試 OpenAI Image API 時因為要用到兩個連動的 gr.Radio 元件, 學到了 gr.update() 函式的用法, 因此對此函式用法做了一番測試並紀錄如下.

本系列全部文章索引參考 :


函式 gr.update() 用來更新元件的屬性, 用來在互動式應用中動態更新 Gradio 使用者介面元件的屬性 (不是重建整個元件), 例如點擊按鈕, 改變選單之選項, 元件預設值, 顯示或隱藏元件等等, 無需重新載入整個介面, 其參數結構如下 : 

gr.update(
    value=None,                # 更新元件的值(例如文字、選取項目等)
    choices=None,             # 更新選單元件的選項(例如 Dropdown, Radio 等)
    visible=True,               # 是否顯示元件(True=顯示, False=隱藏)
    interactive=True,         # 是否可互動(False=禁用元件)
    label=None,                 # 更新元件的標籤文字
    info=None,                  # 更新元件下方的說明文字
    elem_id=None,             # 更新 HTML 元素 ID
    elem_classes=None,     # 更新 HTML class 名稱(list of str)
    container=None,           # 是否包裝在容器中(通常用不到)
    scale=None,                  # 控制元件所佔水平空間比例
    min_width=None,        # 設定元件最小寬度(px)
    render=True                 # 是否渲染此變更(預設 True)
    )

參數說明如下表 :


參數名稱 說明
value 更新元件的值,例如文字內容、選取項目或輸入數字等
choices 適用於 Dropdown、Radio 等元件,用來更新選項列表
visible 是否顯示元件,True 為顯示,False 為隱藏
interactive 是否可互動,False 時會讓元件變成唯讀或無法點擊
label 更新元件的標籤文字
info 更新元件下方顯示的說明資訊
elem_id 更新元件的 HTML 元素 ID,可用於自訂樣式或 JS 操作
elem_classes 更新 HTML class 名稱,可為字串或字串列表,用於自訂樣式
container 是否顯示外層容器,通常保留預設值即可
scale 控制元件在橫向空間中所佔比例,用於區塊佈局(0~1)
min_width 設定元件最小寬度(像素),超過該寬度才會換行
render 是否立即渲染更新,預設為 True


下列 UI 元件之事件方法常用來觸發呼叫 gr.update() 函式去動態更新元件之屬性 :


常用事件方法 說明
click() 按下按鈕時觸發,常用於 gr.Button
change() 當元件值改變時觸發,例如 Dropdown、Radio、Slider 等
submit() 當使用者在 Textbox 按 Enter 鍵時觸發
blur() 當元件失去焦點(如離開輸入框)時觸發
input() 使用者輸入時即時觸發(如每輸入一字)
release() 滑鼠釋放時觸發,常用於 Slider 或繪圖板元件


最常用的是按鈕元件的 click() 或 submit() 方法與選項元件 (Radio, Checkbox, Dropdown) 勾選動作觸發的 change() 方法. 最重要的一點是, UI 元件必須放在 gr.Blocks 語境內才能動態更新, gr.Interface 只處理輸入到輸出的靜態網頁, 無法做元件屬性變更. 

例如 : 


測試 1 : 按鈕的 click 事件更新文字框之顯示或隱藏狀態 (1) [看原始碼]

# gradio-update-test-2.py
import gradio as gr

def show_textbox():
    return gr.update(visible=True)  # 傳回 Update 物件

def hide_textbox():
    return gr.update(visible=False)  # 傳回 Update 物件

with gr.Blocks() as blocks:
    show_btn=gr.Button('顯示輸入框')
    hide_btn=gr.Button('隱藏輸入框')
    textbox=gr.Textbox(label='輸入框', visible=False)  # 文字框預設為隱藏
    show_btn.click(fn=show_textbox, inputs=[], outputs=textbox)
    hide_btn.click(fn=hide_textbox, inputs=[], outputs=textbox)
blocks.launch()

此例用兩個按鈕 show_btn 與 hide_btn 分別用來控制文字框的顯示與隱藏 (文字框預設為隱藏), 當按下這兩個按鈕時會分別呼叫 show_textbox() 與 hide_text(), 這兩個函式都是呼叫 gr.update() 後將結果傳回給 outputs 所指定的元件 (即 show_textbox 與 hide_textbox 文字框), gr.update() 會傳回一個攜帶指定屬性 visible 的 Update 物件, 文字框收到此 Update 物件後就會根據此 Update 物件之屬性去修改自己的屬性 (即 visible=True/False), 所以 Update 物件相當於一個 modify 指令. 注意, 此處因為 click 事件的處理函式不需要輸入元件的資訊, 因此 inputs 傳入空串列即可. 結果如下 : 

按上面按鈕顯示輸入框 :




按下面按鈕隱藏輸入框 :




其實可以只用一個按鈕達成同樣功能, 例如 : 


測試 2 : 按鈕的 click 事件更新文字框之顯示或隱藏狀態 (2) [看原始碼]

# gradio-update-test-2.py
import gradio as gr

def handler(showing):
    new_state=not showing  # 狀態交替:True → False、False → True
    return (  # 根據新狀態回傳更新指令 (傳回 Update 與 State 物件)
        gr.update(visible=new_state),  # 更新 textbox 是否顯示
        gr.update(value='隱藏文字框' if new_state else '顯示文字框'),  # 更新按鈕文字
        new_state  # 更新狀態紀錄
        )

with gr.Blocks() as blocks:
    state=gr.State(False)  # 初始狀態為 False(隱藏文字框)
    btn=gr.Button('顯示文字框')
    textbox=gr.Textbox(label='輸入框', visible=False)  # 文字框預設隱藏
    # outputs: textbox, btn, state (順序對應 handler 回傳值)
    btn.click(fn=handler, inputs=state, outputs=[textbox, btn, state])
blocks.launch()

此例僅用一個按鈕 btn 來控制輸入框之顯示, 並使用 State 物件紀錄文字框的狀態, 當按下按鈕時將此物件傳給 inputs 參數, 讓 handler() 依據此 State 物件來交替狀態, 注意, handler() 的傳回值 (兩個 Update 物件與一個 State 物件) 順序必須對應 click() 的 outputs 參數串列之順序. 當 outputs 所列的輸出元件收到這三個物件後會根據物件內容更新自己的屬性值 (True/False), 結果如下 :




也可以用 gr.Checkbox 元件來控制輸入框顯示與否, 例如 :


測試 3 : 選項元件的 change 事件更新文字框之顯示或隱藏狀態 (2) [看原始碼]

# gradio-update-test-3.py
import gradio as gr

def handler(showing):  # 傳入輸入元件之值 True/False
    return gr.update(visible=showing)  # 傳回 Update 物件

with gr.Blocks() as blocks:
    checkbox=gr.Checkbox(label='顯示輸入框')
    textbox=gr.Textbox(label='輸入框', visible=False)  # 預設隱藏
    checkbox.change(fn=handler, inputs=checkbox, outputs=textbox)
blocks.launch()

此例利用 gr.Checkbox 元件有無勾選來控制文字輸入框之顯示與否, 勾選動作會觸發 change 事件呼叫 handler() 函式來處理, 傳入參數 inputs 就是這個 Checkbox 物件, change() 會先抽取此物件之值 (True/False) 再傳給 handler(), 因此 handler() 接收到的 showing 參數是 True/False, 它會傳回一個攜帶 visible 屬性的 Update 物件, 傳回給 outputs 所指的輸入框物件 textbox 後即依據此屬性更改輸入框的 visible 狀態, 結果如下 :




Checjbox 預設不勾選, 輸入框是隱藏的; 勾選後就會顯示輸入框了. 

2025年6月27日 星期五

如何在 Hugging Face Spaces 平台佈署 Streamlit 網頁應用程式

在前一篇測試中我們利用 Streamlit 在本機建構了一個 web app 讓使用者輸入 OpenAI 金鑰與提示詞呼叫 Image API 來生圖, 參考 :


本篇旨在將此 web app 佈署至 Hugging Face Spaces 平台上, 作法參考 :


首先登入 Hugging Face Spaces 平台 : 


按右上角的 "+New Space" 鈕 : 




輸入空間名稱, 描述, 授權方式等欄位, 點選 Gradio SDK (之前有 Streamlit SDK 可選, 但最近被拿掉, 有可能是改版或維護而暫時下架, 故此處先用 Gradio SDK, 後續再修改 README.md 等設定), 按底下的 "Create Space" 鈕建立空間 : 





按右上角的 "File" 顯示檔案列表 :




點選 README.md 檔案 :




因為建立空間時點選 Gradio SDK, 故裡面的設定為 Gradio 的版本, 按 "Edit" 鈕進入編輯頁面 : 




將其中 SDK 改為 Streamlit 的版本 :

sdk: streamlit  
sdk_version: 1.35.0  
app_file: app.py  

改好後按左下角的 "Commit changes to main" 鈕提交更新 : 





更新結果頁面出現一個提示, 說 Streamlit 有更新的 v1.46.1 可用, 所以再次按 'edit' 修改版本 : 




改好後同樣按左下角的 "Commit changes to main" 鈕提交更新. 

按 main 後面的空間名稱回到檔案列表頁 :




按右上角 "+Contribute" 鈕點選彈出選單的 "Create a new file" : 




在上方文字框輸入 app.py (這是預設應用程式名稱), 然後將 web app 程式 streamlit_openai_image_api_test_1.py 貼到下方 Edit 輸入框裡面後, 按左下角的 "Commit changes to main" 鈕提交建立檔案 : 



  
同樣按 main 後面的空間名稱回到檔案列表頁, 可見 app.py 已在列表中. 因為此 web app 有用到 Hugging Face Spaces 平台沒有預先安裝的第三方套件 openai, 必須新增一個 requirements.txt 將 openai 列在裡面讓平台去安裝, 按右邊 "+Contribute" 鈕點選彈出選單的 "Create a new file" : 




在上方文字框輸入 requirements.txt, 在下方 Edit 文字框輸入 openai, 按左下角的 "Commit changes to main" 鈕提交建立檔案 : 




這樣就完成全部設定了 (Streamlit 不用列在 requirements.txt 裡面, 因為 Hugging Face Spaces 本身有預先安裝), 按 Spaces 後面的空間名稱即可執行此 web app :




結果如下 :




等 Hugging Face Spaces 平台重新將 Streamlit SDK 上架就不需要去修改 README.md 檔了. 

OpenAI API 學習筆記 : 呼叫 Image API 生圖 (三)

在前一篇測試裡, 我們使用 Gradio 作為 WebUI 工具, 在頁面中輸入金鑰與提示詞呼叫 OpenAI Image API 來生成圖像, 本篇則是要改用 Streamlit 來實作功能相同的 Web app. 

本系列全部測試筆記參考 :


關於 Streamlit 套件用法參考 : 


本篇要改寫的對象是前一篇測試中, 模型選單與尺寸選單有連動功能的 gradio-openai-image-api-test-2.py 這個 web app :

import gradio as gr
from openai import OpenAI

# 圖片尺寸選項
size_options={
    'dall-e-2': ['256x256', '512x512', '1024x1024'],
    'dall-e-3': ['1024x1024', '1024x1792', '1792x1024']
    }

# 呼叫 OpenAI API 生圖
def generate_images(prompt, api_key, **kwargs):
    client=OpenAI(api_key=api_key)
    replies=client.images.generate(prompt=prompt, **kwargs)
    urls=[item.url for item in replies.data]
    return urls  # Gallery 只能接收串列

# 執行按鈕時處理函式
def handler(api_key, prompt, model_sel, size_sel, image_count):
    if not api_key.strip():   # 處理使用者未輸入金鑰問題
        return [], '請輸入 OpenAI API Key'
    if not prompt.strip():   # 處理使用者未輸入提示詞問題
        return [], '請輸入提示詞 (prompt)'
    if model_sel == 'dall-e-2':
        size=size_sel
        n=image_count   # dall-e-2 允許生 1~10 張圖
    else:
        size=size_sel
        n=1   # dall-e-3 只允許生 1 張圖
    urls=generate_images(prompt, api_key, model=model_sel, n=n, size=size)
    msg=f'模型: {model_sel}\n尺寸: {size}\n張數: {n}'   # 輸出設定值
    return urls, msg  # 傳回生圖之網址串列給 Gallery, 設定值給狀態訊息

# 模型選單連動圖片尺寸選單 : 動態更新圖片尺寸選單
def update_size_options(selected_model):
    return gr.update(
        choices=size_options[selected_model],
        value=size_options[selected_model][0]
        )

# 使用 Blocks 語法 (才有元件連動功能)
with gr.Blocks(title='OpenAI Image API 測試') as blocks:
    gr.Markdown('## 🎨 使用 DALL·E 2 / 3 生圖')
    api_key=gr.Textbox(label='請輸入金鑰 (API key)', type='password')
    prompt=gr.TextArea(label='請輸入提示詞 (Prompt)', max_lines=10)
    model_sel=gr.Radio(
        label='選擇模型',
        choices=['dall-e-2', 'dall-e-3'],
        value='dall-e-3'
        )
    size_sel=gr.Radio(
        label='選擇圖片尺寸',
        choices=size_options['dall-e-3'],
        value=size_options['dall-e-3'][0]
        )
    image_count=gr.Slider(
        label='圖片張數 (DALL·E 2 專用)',
        minimum=1,
        maximum=10,
        step=1,
        value=1
        )
    submit_btn=gr.Button('開始生成')
    gallery=gr.Gallery(label='生成的圖片')
    status=gr.Textbox(label='狀態訊息', interactive=False) # 僅輸出
    # 連動更新圖片尺寸選單
    model_sel.change(
        fn=update_size_options,  # 呼叫自訂函式更新尺寸選單內容
        inputs=model_sel,  # 模型選單值
        outputs=size_sel   # 尺寸選單值
        )
    # 點擊按鈕觸發 handler
    submit_btn.click(
        fn=handler,
        inputs=[api_key, prompt, model_sel, size_sel, image_count],
        outputs=[gallery, status]
        )
blocks.launch()

此 Gradio 應用程式中利用 gr.update() 函式來動態更新 gr.Radio 元件的選項內容, 當選擇模型時會改變尺寸選擇器的選項. 在 Streamlit 並無類似的函式, 但可以利用 st.session_state 紀錄即時的選項狀態, 當主選單之選擇變化時重新渲染副選單之選項, 作法參考下面這篇的範例 7 :


Streamlit 版的 OpenAI Image API 生圖網頁介面程式如下 :


測試 1 : 模型與尺寸選擇器連動的 Streamlit 生圖介面 [看原始碼]

# streamlit_openai_image_api_test_1.py
import streamlit as st
from openai import OpenAI

# 生圖主函式
def generate_images(prompt, api_key, **kwargs):
    client=OpenAI(api_key=api_key)
    replies=client.images.generate(prompt=prompt, **kwargs)
    return [item.url for item in replies.data]

# UI 開始
st.set_page_config(page_title='OpenAI Image API 測試', layout='wide')
st.markdown('## 🎨 使用 DALL·E 2 / 3 生圖')
# 輸入金鑰與提示詞
api_key=st.text_input('請輸入金鑰 (API key)', type='password')
prompt=st.text_area('請輸入提示詞 (Prompt)', height=100)
# 圖片尺寸選項
size_options={
    'dall-e-2': ['256x256', '512x512', '1024x1024'],
    'dall-e-3': ['1024x1024', '1024x1792', '1792x1024']
    }
# 初始化 session 狀態
if 'model_sel' not in st.session_state:
    st.session_state.model_sel='dall-e-3'
if 'size_sel' not in st.session_state:
    st.session_state.size_sel=size_options['dall-e-3'][0]
# 模型選單(更新尺寸選單)
model=st.radio(
    '選擇模型',
    ['dall-e-2', 'dall-e-3'],
    index=['dall-e-2', 'dall-e-3'].index(st.session_state.model_sel)
    )
if model != st.session_state.model_sel:
    st.session_state.model_sel=model
    st.session_state.size_sel=size_options[model][0]
# 尺寸選單(依模型變化)
size_sel=st.radio(
    '選擇圖片尺寸',
    size_options[st.session_state.model_sel],
    index=size_options[st.session_state.model_sel].index(st.session_state.size_sel)
    )
st.session_state.size_sel=size_sel
# 圖片張數(僅限 dall-e-2)
image_count=1
if st.session_state.model_sel == 'dall-e-2':
    image_count=st.slider('圖片張數 (DALL·E 2 專用)', 1, 10, value=1)
sel_msg=f'模型: {st.session_state.model_sel} 尺寸: {st.session_state.size_sel}'
st.write(sel_msg)
# 按送出按鈕開始生成圖片
if st.button('開始生成', type='primary'):
    if not api_key.strip():  # 提醒必須輸入金鑰
        st.warning('⚠ 請輸入 OpenAI API Key')
    elif not prompt.strip():  # 提醒必須輸入提示詞
        st.warning('⚠️ 請輸入提示詞 (Prompt)')
    else:
        n=image_count if st.session_state.model_sel == 'dall-e-2' else 1
        with st.spinner('生成圖片中...'):
            try:
                urls=generate_images(prompt, api_key, model=st.session_state.model_sel, size=st.session_state.size_sel, n=n)
                st.success(f'✅ 成功產生 {len(urls)} 張圖片。')
                st.markdown(f'**模型**: {st.session_state.model_sel} **尺寸**: {st.session_state.size_sel} **張數**: {n}')
                cols=st.columns(min(len(urls), 3))
                for i, url in enumerate(urls):
                    with cols[i % len(cols)]:
                        st.image(url)
            except Exception as e:
                st.error(f'❌ 發生錯誤:{e}')

結果如下, 如果沒有輸入金鑰會出現提示訊息  :








輸入金鑰與下列提示詞 :

美麗的亞洲女性,優雅且高貴,柔和的自然光,細緻的五官,富有表情的雙眼配上自然的睫毛,光滑如瓷的肌膚,精緻的骨骼結構,自然的妝容,黑色亮麗的頭髮帶有柔和的波浪,神情寧靜,寫實風格,高解析度,專業人像攝影,淺景深,溫暖的色調,電影感燈光,傑作級品質,極致細緻。

用預設的 dall-e-3 模型生成 1024x1024 一張, 按生成鈕後 st.spinner 元件會在頁面顯示一個旋轉小圖表示正在生成圖片 :




當生成完畢時 st.spinner 元件會消失並展示生成之圖片 :




改選 dall-e-2 模型要求生成兩張 512x512 圖像 :





此 web app 我已佈署於 Hugging Face Spaces 平台 : 


2025年6月26日 星期四

如何在 Hugging Face Spaces 平台佈署 Gradio 網頁應用程式

Hugging Face Spaces 是AI 開源平台 Hugging Face 提供的免費雲端 Python Web app 主機服務 (也有提供收費方案), 我曾經在測試 Gradio 時紀錄佈署的方法, 參考 :


不過目前網頁似乎有些小更動, 所以下面以昨天用 Gradio 寫的 OpenAI Image API 測試程式為例, 重新寫一篇說明如何將 web app 佈署到 Hugging Face Spaces 平台上. 

首先須註冊 Hugging Face 帳號 : 


登入後點選右上角的 "Spaces" 超連結 : 




按右上角 "+ New Space" 按鈕 : 




填寫 Space Name 與 Description 欄位, 勾選授權方式, 點選 Gradio SDK 後按左下角的 "Create Space" 鈕建立託管空間 :





完成後會自動進入此空間之頁面, 按右上角的三個小點按鈕, 點選彈出選單中的 File 進入檔案管理頁面 : 




按右方的 "+Contribute" 按鈕, 點選彈出選單中的 "Create a new file" : 




在空間名稱後面的文字框輸入 app.py, 這是 web app space 執行時預設會去尋找的主程式名稱 (可以用其他名稱, 但必須先在 README.md 或 .huggingface.yml 指定 app_file 主程式檔名), 然後將 web app 程式貼在下方程式輸入區, 然後按左下方的 "Commit new file to main" 鈕即可 :





這樣便新增了主程式 app.py, 按上方 Spaces 後面的空間名稱即可執行 web app; 按下方 main 後面的專案名稱則會顯示此空間內的檔案列表 : 




但這個 App 有用到平台未預先安裝的套件 openai, 所以直接執行會出現錯誤, 按右上角的三個小點按鈕點選 Files, 這樣也會顯示檔案列表 : 




然後重複上面新增 app.py 的做法, 按右方的 "+Contribute" 按鈕, 點選彈出選單中的 "Create a new file", 在上面檔案名稱文字框輸入 requirements.txt, 在下方 Edit 文字框輸入 openai,  按左下方的 "Commit new file to main" 鈕即可建立此套件安裝檔 : 





這時按 Spaces 後面的空間名稱就可以順利執行此 web app 了 :





這樣便完成 web app 的佈署了 : 


全部 app 列表參考 :


高科大還書 1 本 : 一本搞定生成式AI

週二中午回母校圖書館還這本書 (被預約) 同時取另一本到館書 : 


結果在書架上看到同樣的這本書, 傻眼, 但因為取到館書後又滿借了, 先記下來等下次再借. 此書篇幅不大, 很簡潔地介紹了目前火紅的幾款 AI 工具, 例如 Pika, Runway 等. 我其實很想借下面這本 :


可惜尚未上架無法借. 若不是考量家中書架已爆滿, 真的想乾脆上 momo 買一本. 

還完書照例去鄧園吃雞腿飯, 我發現自助餐裡 10 個有 8 個編吃飯邊滑手機, 現在的人都已經不知道生活的味道了, 吃個飯還這麼忙. 

OpenAI API 學習筆記 : 呼叫 Image API 生圖 (二)

在前一篇測試中已對 OpenAI Image API 做過初步測試, 本篇要使用 Gradio 做為呼叫 Image API 生圖的使用者介面. 

本系列全部測試筆記參考 :


關於 Gradio 套件用法參考 : 


下面範例以直觀的作法處理模型與尺寸選擇器 : 


測試 1 : 模型與尺寸選擇器不連動的生圖介面 [看原始碼]

# gradio-openai-image-api-test-1.py
import gradio as gr
from openai import OpenAI

def generate_images(prompt, api_key, **kwargs):
    client=OpenAI(api_key=api_key)
    replies=client.images.generate(prompt=prompt, **kwargs)
    urls=[item.url for item in replies.data]
    return urls

def handler(api_key, prompt, model_sel, size_sel, image_count):
    if not api_key.strip():  # 處理使用者未輸入金鑰問題
        return [], '請輸入 OpenAI API Key'
    elif not prompt.strip():  # 處理使用者未輸入提示詞問題
        return [], '請輸入提示詞 (prompt)'
    if model_sel == 'dall-e-2':  
        n=image_count  # dall-e-2 允許生 1~10 張圖
        if size_sel == 'dall-e-2:256x256|dall-e-3:1024x1024':
            size='256x256'
        elif size_sel == 'dall-e-2:512x512|dall-e-3:1024x1792':
            size='512x512'
        else:
            size='1024x1024'
    else:  # model_sel='dall-e-3'
        n=1  # dall-e-3 只允許生 1 張圖
        if size_sel == 'dall-e-2:256x256|dall-e-3:1024x1024':
            size='1024x1024'
        elif size_sel == 'dall-e-2:512x512|dall-e-3:1024x1792':
            size='1024x1792'
        else:
            size='1792x1024'
    urls=generate_images(prompt, api_key, model=model_sel, n=n, size=size)
    msg=f'model:{model_sel}\nsize:{size}\nn:{n}'  # 輸出設定值
    return urls, msg  # 傳回生圖之網址給 Gallery, 設定值給狀態訊息

api_key=gr.Textbox(label='請輸入金鑰 (API key)', type='password')
prompt=gr.TextArea(label='請輸入提示詞 (Prompt)', max_lines=10)
model_sel=gr.Radio(
    label='選擇模型',
    choices=['dall-e-2', 'dall-e-3'],
    value='dall-e-3'
    )
size_sel=gr.Radio(
    label='選擇圖片尺寸',
    choices=[
        'dall-e-2:256x256|dall-e-3:1024x1024',
        'dall-e-2:512x512|dall-e-3:1024x1792',
        'dall-e-2:1024x1024|dall-e-3:1792x1024'
        ],
    value='dall-e-2:256x256|dall-e-3:1024x1024'
    )
image_count=gr.Slider(
    label='圖片張數',
    minimum=1,
    maximum=10,
    step=1,
    value=1
    )
iface=gr.Interface(
    fn=handler,
    inputs=[api_key, prompt, model_sel, size_sel, image_count],  
    outputs=[
        gr.Gallery(label='生成的圖片'),
        gr.Textbox(label='狀態訊息', interactive=False)
        ],
    title='OpenAI Image API 測試',
    flagging_mode='never'
    )
iface.launch()

此例使用兩個 Radio 單選圓鈕來選擇生圖模型與圖片尺寸, 因為 dall-e-2 與 dall-e-3 分別有三種不同尺寸, 故使用 if else 判斷 model_sel 與 size_sel 來決定 size 之值. 為了避免使用者未輸入金鑰或提示詞, 在 handler() 中一開始便檢查這兩個欄位是否為空字串, 是的話就終止生圖並於狀態資訊欄顯示原因. 

輸入前一篇測試使用的描繪亞洲女性臉孔的提示詞 (使用中文版) :

美麗的亞洲女性,優雅且高貴,柔和的自然光,細緻的五官,富有表情的雙眼配上自然的睫毛,光滑如瓷的肌膚,精緻的骨骼結構,自然的妝容,黑色亮麗的頭髮帶有柔和的波浪,神情寧靜,寫實風格,高解析度,專業人像攝影,淺景深,溫暖的色調,電影感燈光,傑作級品質,極致細緻。

使用 dall-e-3 模型繪製 1024x1024 一張結果如下 :




使用 dall-e-2 模型繪製 256x256| 兩張結果如下 :




使用 dall-e-2 模型繪製 512x512| 四張結果如下 :




此 web app 已發布於 Hugging Face Spaces :



上面範例中的模型選單與尺寸選單是獨立不連動的, 我們是利用 if else 依據模型選單值來決定尺寸選單要用哪個選項值, 這種做法簡單但介面不太友善, 使用者看到尺寸選單時會有點困惑. 比較友善的做法是要讓這兩個選單連動, 讓尺寸選單依據模型選單的值顯示 dall-e-2 或 dall-e-3 尺寸.

因為 gr.Interface 不支援元件間連動, 必須使用 gr.Blocks() 結構才能動態更新元件 (例如此處的 gr.Radio) 內容, 將上面範例改寫為如下 gr.Blocks() 架構的版本 : 


測試 2 : 模型與尺寸選擇器連動的生圖介面 [看原始碼]

# gradio-openai-image-api-test-2.py
import gradio as gr
from openai import OpenAI

# 圖片尺寸選項
size_options={
    'dall-e-2': ['256x256', '512x512', '1024x1024'],
    'dall-e-3': ['1024x1024', '1024x1792', '1792x1024']
    }

# 呼叫 OpenAI API 生圖
def generate_images(prompt, api_key, **kwargs):
    client=OpenAI(api_key=api_key)
    replies=client.images.generate(prompt=prompt, **kwargs)
    urls=[item.url for item in replies.data]
    return urls  # Gallery 只能接收串列

# 執行按鈕時處理函式
def handler(api_key, prompt, model_sel, size_sel, image_count):
    if not api_key.strip():   # 處理使用者未輸入金鑰問題
        return [], '請輸入 OpenAI API Key'
    if not prompt.strip():   # 處理使用者未輸入提示詞問題
        return [], '請輸入提示詞 (prompt)'
    if model_sel == 'dall-e-2':
        size=size_sel
        n=image_count   # dall-e-2 允許生 1~10 張圖
    else:
        size=size_sel
        n=1   # dall-e-3 只允許生 1 張圖
    urls=generate_images(prompt, api_key, model=model_sel, n=n, size=size)
    msg=f'模型: {model_sel}\n尺寸: {size}\n張數: {n}'   # 輸出設定值
    return urls, msg  # 傳回生圖之網址串列給 Gallery, 設定值給狀態訊息

# 模型選單連動圖片尺寸選單 : 動態更新圖片尺寸選單
def update_size_options(selected_model):
    return gr.update(
        choices=size_options[selected_model],
        value=size_options[selected_model][0]
        )

# 使用 Blocks 語法 (才有元件連動功能)
with gr.Blocks(title='OpenAI Image API 測試') as blocks:
    gr.Markdown('## 🎨 使用 DALL·E 2 / 3 生圖')
    api_key=gr.Textbox(label='請輸入金鑰 (API key)', type='password')
    prompt=gr.TextArea(label='請輸入提示詞 (Prompt)', max_lines=10)
    model_sel=gr.Radio(
        label='選擇模型',
        choices=['dall-e-2', 'dall-e-3'],
        value='dall-e-3'
        )
    size_sel=gr.Radio(
        label='選擇圖片尺寸',
        choices=size_options['dall-e-3'],
        value=size_options['dall-e-3'][0]
        )
    image_count=gr.Slider(
        label='圖片張數 (DALL·E 2 專用)',
        minimum=1,
        maximum=10,
        step=1,
        value=1
        )
    submit_btn=gr.Button('開始生成')
    gallery=gr.Gallery(label='生成的圖片')
    status=gr.Textbox(label='狀態訊息', interactive=False) # 僅輸出
    # 連動更新圖片尺寸選單
    model_sel.change(
        fn=update_size_options,  # 呼叫自訂函式更新尺寸選單內容
        inputs=model_sel,  # 模型選單值
        outputs=size_sel   # 尺寸選單值
        )
    # 點擊按鈕觸發 handler
    submit_btn.click(
        fn=handler,
        inputs=[api_key, prompt, model_sel, size_sel, image_count],
        outputs=[gallery, status]
        )
blocks.launch()

執行後模型與尺寸選單就會連動了, 預設是 dall-e-3 選單 : 




模型改選 dall-e-2 時尺寸選單會更新選項 :




再次生成亞洲女性臉孔 (使用英文提示詞) : 




選擇 dall-e-3 模型生成一張 1024x1024 圖片 :




選擇 dall-e-2 模型生成兩張 1024x1024 圖片 :





此 Gradio 應用程式我已發佈到 Hugging Face Spaces 平台 :