顯示具有 OpenAI 標籤的文章。 顯示所有文章
顯示具有 OpenAI 標籤的文章。 顯示所有文章

2026年9月23日 星期三

OpenAI API 儲值 10 美元

昨天用 LINE 串接測試 OpenClaw 生圖, 發現它特別吃 token, 幾番生圖下來就把剩餘的 3.5 美元吃光, 檢視 OpenAI 的 Billing 頁還欠 0.24 美元 : 




不過用光了也好, 反正去年 10 月儲值 5 美元後一直沒在用, 是最近玩 OpenClaw 才有用, 一年到期如果沒用完會被沒收. 按 "Buy credits" 鈕可再儲值, 考量後續還有很多測試要做, 所以這回儲值 10 美元 (現在最低要儲值 6 美元了) : 





扣掉欠債 0.24 美元, 剩下 9.76 美元, 刷 LINE Pay 卡支付 :





2026-09-30 補充 :

儲值後我只問早安而已, 餘額就從 9.76 美元降至 9.68 美元, 耗掉 0.08 美元, 約合台幣 2.55 元, 似乎不便宜啊! 




2026年8月12日 星期三

OpenAI API 學習筆記 : 在 Win11 安裝 Codex CLI

前陣子 OpenAI 推出 Codex 後看到網路上有非常多討論文章, 感覺似乎其 Coding 能力不輸 Claude Code, 但我一直沒時間試用, 今天抽空來安裝 Codex CLI 玩玩看. 

最近在市圖新書架上看到洪錦魁老師新書 "Vibe Coding - Python超級入門 : ChatGPT x Codex", 在末尾章節有介紹如何安裝 Codex CLI, 但須先安裝 WSL, 用 Windows 附的 Linux 環境來安裝, 但我詢問 Gemini, 發現其實可以利用 Git Bash 介面來跑 Codex CLI, 不需要透過 WSL, 與 Claude Code 類似, 但 Codex CLI 是用 npm 安裝的, 與 Claude Code 用 PS 指令稿不同, 參考 : 



1. 安裝 Scoop, Git, Nvm 與 Node.js : 

首先用一般使用者身份開啟 PS 視窗, 安裝 scoop 後再利用它來安裝 git 與 node, 只要在 PS 視窗依序輸入下列指令安裝即可 :
  • 設定 PowerShell 視窗腳本執行原則 : 
    Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
  • 安裝 scoop : 
    Invoke-RestMethod -Uri https://get.scoop.sh | Invoke-Expression
  • 安裝 git :
    scoop install git  
  • 安裝 nvm : 
    scoop install nvm
  • 用 nvm 安裝 node.js : 
    nvm install node 
我的 LG Gram 筆電之前安裝 Claude Code 就已安裝好 : 

PS C:\Users\tony1> scoop --version  
Current Scoop version:
b588a06e (HEAD -> master, tag: v0.5.3, origin/master, origin/HEAD) chore(release): Bump to version 0.5.3 (resync) (#6436)

'main' bucket:
408b426f8c (HEAD -> master, origin/master, origin/HEAD) sqruff: Update to version 0.34.1

PS C:\Users\tony1> git version  
git version 2.52.0.windows.1

用 where.exe 檢視 Git Bash 指令檔位置 :

PS C:\Users\tony1> where.exe bash   
C:\Users\tony1\scoop\shims\bash.exe

PS C:\Users\tony1> nvm --version  
1.2.2

檢查 nvm 附帶的 npm 工具版本 :

PS C:\Users\tony1> npm --version  
11.13.0

Node.js 之前在安裝 Gemini CLI 時也用 Scoop 安裝好了 :

PS C:\Users\tony1> node --version  
v25.2.1

參考 :



2. 安裝 Codex CLI : 

當以上環境準備好後, 即可用 nvm 附帶的 npm 工具安裝 Codex CLI 了 : 

npm install -g @openai/codex 

PS C:\Users\tony1> npm install -g @openai/codex    

added 2 packages in 1m
npm notice
npm notice New minor version of npm available! 11.13.0 -> 11.19.0
npm notice Changelog: https://github.com/npm/cli/releases/tag/v11.19.0
npm notice To update run: npm install -g npm@11.19.0
npm notice

顯示 "added 2 packages in 1m" 表示 Codex 已安裝成功, 其餘 notice 訊息只是提醒 npm 已有新版可更新而已. 檢查 Codex CLI 版本 : 

PS C:\Users\tony1> codex --version   
codex-cli 0.147.0


3. 執行 Codex CLI : 

在 PS 視窗下 codex 指令即可進入 Codex CLI 介面, 首先會要求登入 ChatGPT 或提供 API Key 才能使用 Codex, 我之前有儲值 API Key, 移動至 3. Provide your own API Key, 按 Enter :




貼上 API Key 後按 Enter :




按 Yes, continue : 




選 2. Use non-admin sandbox :




這時會花幾分鐘去設定沙箱 : 




請耐心等待設定, 完成後即可進行專案開發 :




其實比較正確的做法是要先建立一個專案資料夾, 然後用 cd 進入專案資料夾後再下 codex 指令. 

2026年4月17日 星期五

樹莓派學習筆記 : 在 Bulleye 上安裝 google-generativeai 套件

昨天成功重灌 Pi 3A+ 的 Bulleye 後嘗試安裝 langchain-core, 結果因為版本衝突不順利, 且就算安裝成功, 由於 Pi 3A+ 只有 512MB DRAM, 跑 langchain 太沉重了只好放棄, 改為安裝原生 SDK. 安裝 openai 套件成功且可順利匯入 :

pi@pi3aplus:~ $ pip install openai   
Looking in indexes: https://pypi.org/simple, https://www.piwheels.org/simple
Collecting openai
...(略)...
Installing collected packages: openai
Successfully installed openai-2.32.0
pi@pi3aplus:~ $ python   
Python 3.9.2 (default, Jan 24 2026, 09:41:14) 
[GCC 10.2.1 20210110] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from openai import OpenAI   
>>> exit()  

安裝 Gemini 舊版 API 的 google-generativeai 套件 : 

pi@pi3aplus:~ $ pip install google-generativeai   
Looking in indexes: https://pypi.org/simple, https://www.piwheels.org/simple
Collecting google-generativeai
  Downloading google_generativeai-0.8.6-py3-none-any.whl (155 kB)
...(略)...
Successfully installed google-ai-generativelanguage-0.6.15 google-api-python-client-2.194.0 google-auth-httplib2-0.3.1 google-generativeai-0.8.6 grpcio-1.80.0 grpcio-status-1.71.2 httplib2-0.31.2 protobuf-5.29.6 uritemplate-4.2.0

但用 import google.generativeai as genai 匯入時會出現 grpcio 相關錯誤, 原因也是版本衝突問題, AI 建議改用下列安裝指令鎖住版本 : 

pi@pi3aplus:~ $ pip install "google-generativeai==0.3.1" \
            "google-ai-generativelanguage==0.4.0" \
            "grpcio==1.54.2" \
            "grpcio-status==1.54.2" \
            "protobuf==4.25.3" \
            --force-reinstall --no-cache-dir   
Looking in indexes: https://pypi.org/simple, https://www.piwheels.org/simple
Collecting google-generativeai==0.3.1
  Downloading google_generativeai-0.3.1-py3-none-any.whl (146 kB)
...(略)...
Successfully installed certifi-2026.2.25 cffi-2.0.0 charset-normalizer-3.4.7 cryptography-46.0.7 google-ai-generativelanguage-0.4.0 google-api-core-2.29.0 google-auth-2.49.2 google-generativeai-0.3.1 googleapis-common-protos-1.73.0 grpcio-1.54.2 grpcio-status-1.54.2 idna-3.11 proto-plus-1.27.1 protobuf-4.25.3 pyasn1-0.6.3 pyasn1-modules-0.4.2 pycparser-2.23 requests-2.32.5 tqdm-4.67.3 typing-extensions-4.15.0 urllib3-2.6.3

這樣匯入時就只報出無關緊要的 Warning 了 (這些警告只是提醒 : Python 3.9 太舊了, 以後可能不支援而已) : 

pi@pi3aplus:~ $ python  
Python 3.9.2 (default, Jan 24 2026, 09:41:14) 
[GCC 10.2.1 20210110] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import google.generativeai as genai   
/home/pi/.local/lib/python3.9/site-packages/google/api_core/_python_version_support.py:246: FutureWarning: You are using a non-supported Python version (3.9.2). Google will not post any further updates to google.api_core supporting this Python version. Please upgrade to the latest Python version, or at least Python 3.10, and then update google.api_core.
  warnings.warn(message, FutureWarning)
/home/pi/.local/lib/python3.9/site-packages/google/auth/__init__.py:54: FutureWarning: You are using a Python version 3.9 past its end of life. Google will update google-auth with critical bug fixes on a best-effort basis, but not with any other fixes or features. Please upgrade your Python version, and then update google-auth.
  warnings.warn(eol_message.format("3.9"), FutureWarning)
/home/pi/.local/lib/python3.9/site-packages/google/oauth2/__init__.py:40: FutureWarning: You are using a Python version 3.9 past its end of life. Google will update google-auth with critical bug fixes on a best-effort basis, but not with any other fixes or features. Please upgrade your Python version, and then update google-auth.
  warnings.warn(eol_message.format("3.9"), FutureWarning)

如果不想看到這些警告, 可以在程式開頭用下列程式碼隱藏 :

import warnings
warnings.filterwarnings("ignore", category=FutureWarning)

如果想永久隱藏, 就要用 nano ~/.bashrc 去修改設定檔, 在最底下加上 :

export PYTHONWARNINGS="ignore::FutureWarning"

用下列程式測試可正確載入 API :

import grpc
print(f"GRPC 版本: {grpc.__version__}") # 應該要是 1.54.2
import google.generativeai as genai
print("Gemini SDK 成功載入!")

>>> import google.generativeai as genai    
>>> print(genai.__version__)   
0.3.1
>>> try:
...     print("SDK 載入成功,準備測試屬性...")
...     model = genai.GenerativeModel('gemini-pro')
...     print("模型物件建立成功!")
... except Exception as e:
...     print(f"執行出錯: {e}")
... 
SDK 載入成功,準備測試屬性...
模型物件建立成功!

我把 Gemini API 金鑰放在環境變數檔 .env 裡的 GEMINI_API_KEY 中利用 dotenv 套件讀取後實際呼叫 Gemini API 測試 OK :

>>> import google.generativeai as genai  
>>> from dotenv import dotenv_values   
>>> config=dotenv_values('.env')     
>>> gemini_api_key=config.get('GEMINI_API_KEY')   
>>> genai.configure(api_key=gemini_api_key)    
>>> model=genai.GenerativeModel('gemini-2.5-flash')   
>>> reply=model.generate_content('你是誰?')    
>>> print(reply.text)    
我是一個大型語言模型,由 Google 訓練。
我沒有名字、沒有身體,也沒有個人情感或意識。我的目的是回答你的問題、提供資訊、進行對話,並在各種任務上提供幫助。

下面是 OpenAI API 的測試 :

>>> from openai import OpenAI   
>>> from dotenv import dotenv_values 
>>> config=dotenv_values('.env')
>>> openai_api_key=config.get('OPENAI_API_KEY')    
>>> client=OpenAI(api_key=openai_api_key)   
>>> reply=client.chat.completions.create(   
...     messages=[   
...         {"role": "user",
...          "content": "你是誰?",
...         }],
...     model="gpt-3.5-turbo",
...     )
>>> print(reply.choices[0].message.content)
我是一個AI人工智能助手,可以與你進行對話、回答問題和提供信息。有什麼我可以幫助你的嗎?

2026年4月10日 星期五

AI 應用程式專案 (二) : Youtube 字幕摘要生成器

本篇繼續測試 Oreilly "AI 應用程式開發" 這本書第三章的 App 專案 No.2 : Youtube 影片摘要, 本篇旨在測試如何利用第三方套件抓取 Youtube 影片字幕後丟給 AI 生成影片內容摘要. 此書的範例程式可在 GitHub 下載 :


本專案範例原始碼網址 : 


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



1. 安裝 Youtube 字幕抓取工具 yt-dlp :

我詢問 Gemini 要如何下載 YT 字幕檔, 它推薦用 youtube-transcript-api, 但經測試發現無法下載字幕, 很可能是被 YT 阻擋了, 第二選擇是使用 yt-dlp 套件, 經測試可順利下載字幕檔. 

首先用 pip 安裝此套件 : 

(myvenv) D:\python\test>pip install yt-dlp   
Collecting yt-dlp
  Downloading yt_dlp-2026.3.17-py3-none-any.whl.metadata (182 kB)
Downloading yt_dlp-2026.3.17-py3-none-any.whl (3.3 MB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 3.3/3.3 MB 7.5 MB/s  0:00:00
Installing collected packages: yt-dlp
Successfully installed yt-dlp-2026.3.17

我找了一個含有中英文字幕的 Python 教學短片來測試 :


測試程式如下 (ChatGPT 生成) : 

# get_youtube_transcript_1.py
import sys
import os
import yt_dlp

def get_yt_subtitle_ytdlp(video_id):
    url=f"https://www.youtube.com/watch?v={video_id}"
    # 設定 yt-dlp 參數
    ydl_opts={
        'skip_download': True,        # 不下載影片檔
        'writesubtitles': True,       # 抓取手寫字幕
        'writeautomaticsub': True,    # 如果沒手寫就抓自動生成的
        'subtitleslangs': ['zh-Hant', 'zh-TW', 'en'], # 語言優先順序
        'outtmpl': '%(title)s.%(ext)s',  # 設定輸出檔名主檔名為影片標題
        'quiet': True,
        'no_warnings': True,
        }
    try:
        with yt_dlp.YoutubeDL(ydl_opts) as ydl:
            print(f"DEBUG: 正在透過 yt-dlp 請求影片 {video_id} 的資訊...")
            info=ydl.extract_info(url, download=False)
            subtitles=info.get('requested_subtitles')
            if subtitles:
                for lang, sub_info in subtitles.items():
                    print(f"✅ 成功找到語言: {lang}")
                # 下載字幕檔
                ydl.download([url])
                # 取得檔名(影片檔名 base)
                base_filename=ydl.prepare_filename(info)
                base_name=os.path.splitext(base_filename)[0]
                # 嘗試找字幕檔
                found_files=[]
                for lang in subtitles.keys():
                    possible_file=f"{base_name}.{lang}.vtt"
                    if os.path.exists(possible_file):
                        found_files.append(possible_file)
                if found_files:
                    for f in found_files:
                        print(f"🎉 字幕已下載: {f}")
                else:
                    print("⚠️ 字幕下載完成,但找不到實際檔案名稱")
            else:
                print("❌ 找不到符合的繁體中文或英文字幕")
    except Exception as e:
        print(f"❌ yt-dlp 抓取失敗: {e}")

if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("用法: python script.py [影片ID]")
        sys.exit(1)
    get_yt_subtitle_ytdlp(sys.argv[1])

執行結果如下 : 

(myvenv) D:\python\test>python get_youtube_transcript_2.py OndBl1H1rwM    
DEBUG: 正在透過 yt-dlp 請求影片 OndBl1H1rwM 的資訊...
✅ 成功找到語言: zh-TW
✅ 成功找到語言: en
🎉 字幕已下載: 【Code Gym】Python基礎教學(5) - for迴圈和while迴圈.zh-TW.vtt
🎉 字幕已下載: 【Code Gym】Python基礎教學(5) - for迴圈和while迴圈.en.vtt

開啟檢視繁中字幕檔內容 : 

WEBVTT
Kind: captions
Language: zh-TW

00:00:05.940 --> 00:00:10.400
我們撰寫程式的目的,除了是要建立商業邏輯中判斷的條件

00:00:10.400 --> 00:00:12.960
還需要善用電腦快速運算的能力

00:00:13.140 --> 00:00:16.320
在商業邏輯中執行反覆出現的規則運算

00:00:16.320 --> 00:00:20.820
其中for迴圈和while迴圈就是我們兩個好用的工具

00:00:21.160 --> 00:00:23.680
如果你想要指定程式執行的次數

00:00:23.680 --> 00:00:27.820
或是從容器型態的物件中依序取出裡面的值

00:00:27.820 --> 00:00:31.060
像是我先前介紹過的List, Tuple型態

... (略) ...

00:08:12.700 --> 00:08:17.100
Code Gym頻道主要是分享程式語言教學和電腦網路相關知識

00:08:17.100 --> 00:08:20.900
像是今天影片中所介紹的「for迴圈和while迴圈」

00:08:21.880 --> 00:08:24.240
如果你想要收到最新影片消息

00:08:24.240 --> 00:08:26.080
歡迎訂閱Code Gym頻道

00:08:26.080 --> 00:08:27.020
開小鈴鐺

00:08:27.020 --> 00:08:29.020
我們下次再見,掰掰!

但上面程式有一個缺點, 字幕檔的主檔名使用影片標題, 這可能在之後要用程式開啟檔案時帶來麻煩 (例如標題中有怪碼), 比較好的做法是用影片 ID 當主檔名, 只要修改 yt-dlp 參數中的 'outtmpl' 鍵為 '%(id)s.%(ext)s' 即可 :

'outtmpl': '%(id)s.%(ext)s'

再次執行結果如下 : 

(myvenv) D:\python\test>python get_youtube_transcript_1.py OndBl1H1rwM   
DEBUG: 正在透過 yt-dlp 請求影片 OndBl1H1rwM 的資訊...
✅ 成功找到語言: zh-TW
✅ 成功找到語言: en
🎉 字幕已下載: OndBl1H1rwM.zh-TW.vtt
🎉 字幕已下載: OndBl1H1rwM.en.vtt


2. 串接 OpenAI API 生成影片字幕摘要 :

在上面下載字幕檔程式的基礎上, 將字幕內容經過清理, 去除文字以外的資訊後丟給 GPT 模型生成摘要, 程式碼如下 : 

# get_youtube_transcript_2.py
import sys
import os
import re
import yt_dlp
from openai import OpenAI
from dotenv import dotenv_values

config=dotenv_values('.env') 
openai_api_key=config.get('OPENAI_API_KEY')
client=OpenAI(api_key=openai_api_key)

def clean_vtt(file_path):
    """
    清理 VTT 字幕檔,移除時間軸、標頭與重複的文字區塊,回傳純文字。
    """
    if not os.path.exists(file_path):
        return ""
    with open(file_path, 'r', encoding='utf-8') as f:
        lines=f.readlines()
    clean_text_list=[]
    for line in lines:
        # 移除 WEBVTT 標頭、時間軸 (-->) 與設定行
        if "-->" in line or line.startswith("WEBVTT") or line.startswith("Kind:") or line.startswith("Language:"):
            continue
        # 移除 HTML 標籤 (例如 <c> 標籤)
        line=re.sub(r'<[^>]+>', '', line).strip()
        # 避免加入空白行與重複的行 (VTT 常有重複出現的字幕快照)
        if line and (not clean_text_list or line != clean_text_list[-1]):
            clean_text_list.append(line)
    return "\n".join(clean_text_list)

def ask_gpt(
    messages: list[dict[str, str]],
    model: str='gpt-3.5-turbo'
    ) -> str:
    try:
        reply=client.chat.completions.create(
            model=model, 
            messages=messages
            )
        return reply.choices[0].message.content or ''
    except APIError as e:
        return e.message

def summarizer(text):
    if not text:
        return "無字幕內容可生成摘要。"
    print("\n--- [摘要生成中] ---")
    print(f"(已接收到 {len(text)} 字的字幕內容,準備進行摘要...)")
    # 呼叫 AI 生成摘要
    return ask_gpt([{"role": "user",
                     "content": f"請摘要下列字幕內容 : \n{text}"}])

def get_yt_subtitle_ytdlp(video_id):
    url=f"https://www.youtube.com/watch?v={video_id}"
    # 定義語言優先順序:繁體中文 -> 簡體中文 -> 英文
    lang_priority=['zh-Hant', 'zh-TW', 'zh-Hans', 'zh-CN', 'en']
    ydl_opts={
        'skip_download': True,
        'writesubtitles': True,
        'writeautomaticsub': True,
        'subtitleslangs': lang_priority, 
        'outtmpl': '%(id)s.%(ext)s',  # 強制以影片 ID 為主檔名
        'quiet': True,
        'no_warnings': True,
        }
    try:
        with yt_dlp.YoutubeDL(ydl_opts) as ydl:
            print(f"DEBUG: 正在透過 yt-dlp 請求影片 {video_id} 的資訊...")
            info=ydl.extract_info(url, download=False)
            subtitles=info.get('requested_subtitles')
            if not subtitles:
                print("❌ 找不到符合要求的字幕。")
                return
            # 下載字幕檔
            ydl.download([url])
            # 依照優先順序尋找已下載的檔案
            selected_file=None
            for lang in lang_priority:
                possible_file=f"{video_id}.{lang}.vtt"
                if os.path.exists(possible_file):
                    selected_file=possible_file
                    print(f"✅ 已選定最優語言字幕: {lang} ({selected_file})")
                    break
            if selected_file:
                # 1. 清理字幕
                print(f"🧹 正在清理字幕格式...")
                cleaned_content=clean_vtt(selected_file)
                # 2. 生成摘要
                summary_result=summarizer(cleaned_content)
                print("\n[摘要結果]:")
                print(summary_result)
                # 可選:實驗完成後刪除暫存的 vtt 檔
                # os.remove(selected_file)
            else:
                print("⚠️ 檔案下載完成,但讀取時找不到檔案。")
    except Exception as e:
        print(f"❌ 執行過程中發生錯誤: {e}")

if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("用法: python script.py [影片ID]")
        sys.exit(1)
    get_yt_subtitle_ytdlp(sys.argv[1])

執行結果如下 : 

(myvenv) D:\python\test>python get_youtube_transcript_2.py OndBl1H1rwM   
DEBUG: 正在透過 yt-dlp 請求影片 OndBl1H1rwM 的資訊...
✅ 已選定最優語言字幕: zh-TW (OndBl1H1rwM.zh-TW.vtt)
🧹 正在清理字幕格式...

--- [摘要生成中] ---
(已接收到 2339 字的字幕內容,準備進行摘要...)

[摘要結果]:
本文介紹了在撰寫程式中使用for迴圈和while迴圈的基本概念和用法。for迴圈主要用於從容器型態中依序取出值,可以指定程式執行的次數或範圍,使用range()函式可以簡化處理。在for迴圈中,可以使用break和continue來控制迴圈的流程。而while迴圈則是根據條件式的判斷結果來決定是否執行程式區塊,可以用來進行猜數字等互動式遊戲。最後,介紹了如何匯入Python模組,在學習完本文後可以在程式編輯軟體上實際練習程式碼。

2026年4月9日 星期四

AI 應用程式專案 (一) : 新聞稿生成器

我的 OpenAI API 帳戶自去年再次儲值 5 美元後, 只做了少許生圖測試便晾在一邊, 轉眼已過了大半年, 為了避免一年使用期限到期沒用完被沒收, 打算拿 Oreilly "AI 應用程式開發" 這本書第三章的六個 App 專案來消耗掉. 

測試環境使用最近為了 LangChain 測試而建立的乾淨虛擬環境, 參考 :


此書的範例程式可在 GitHub 下載 :


本篇旨在測試第三章中的專案 1 : 打造一個新聞稿生成器, 書中範例原始碼參考 : 


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


此專案的目的是建立一個 AI 應用程式, 可以指定文章長度, 語氣與風格來生成新聞稿. 書中範例程式使用了 typing.List 來進行類型提示 (Type Hinting), 例如 facts: List[str] 是告訴閱讀程式碼的人或 IDE : facts 這個參數應該是一個串列, 且這個串列裡面的每個元素都應該是字串. 不過, 這種用法在 Python 3.9 之後已經內建到 list 中了, 新寫法是 facts: list[str], 所以我將原始範例程式碼改寫為如下 :

# news_generator_1.py
from openai import OpenAI
from dotenv import dotenv_values

config=dotenv_values('.env') 
openai_api_key=config.get('OPENAI_API_KEY')
client=OpenAI(api_key=openai_api_key)

def ask_gpt(
    messages: list[dict[str, str]],
    model: str = 'gpt-3.5-turbo'
    ) -> str:
    try:
        reply=client.chat.completions.create(
            model=model, 
            messages=messages
            )
        return reply.choices[0].message.content or ''
    except APIError as e:
        return e.message

prompt_role='''You are an assistant for journalists. 
Your task is to write articles, based on the FACTS that are given to you. 
You should respect the instructions: the TONE, the LENGTH, and the STYLE'''
# 你是記者的助理. 
# 你的任務是根據所提供的事實 (FACTS) 來撰寫一篇新聞稿.
# 文章需符合指定的語氣 (TONE), 長度 (LENGTH), 與風格 (STYLE). 

def assist_journalist(
    facts: list[str], 
    tone: str, 
    length_words: int, 
    style: str
    ) -> str:
    """根據傳入的事實、語氣, 長度與風格生成新聞稿"""
    facts_str=", ".join(facts)
    prompt=f'{prompt_role}\nFACTS: {facts_str}\nTONE: {tone}\nLENGTH: {length_words} words\nSTYLE: {style}'
    return ask_gpt([{"role": "user", "content": prompt}])

# 執行範例
if __name__ == "__main__":
    result=assist_journalist(
        facts=[
            '2026年東京櫻花預計在3月底滿開', 
            '上野公園是著名的賞櫻勝地,有超過1000棵櫻花樹', 
            '傍晚會有點燈活動,可以欣賞浪漫的夜櫻',
            '請使用繁體中文撰寫'  
            ],
        tone='浪漫且活潑', 
        length_words=300, 
        style='旅遊部落格文章'
        )
    print(result)

此處兩個函式 assist_journalist() 與 ask_gpt() 的傳入參數都使用了類型提示語法以增加程式碼可讀性. 例如 ask_gpt() 中的 messages: list[dict[str, str]] 意思是 :
  • messages 是一個串列, 裡面的每個元素都是字典.
  • 字典的鍵與值都是字串, 例如 {"role": "user", "content": "hello"}
而 model: str = 'gpt-3.5-turbo' 表示 model 是一個字串且有預設值. -> str 表示傳回值類型為字串. 

執行結果如下 :

(myvenv) D:\python\test>python news_generator_1.py   
東京櫻花季即將來臨!2026年的櫻花季預計在3月底盛大開放,其中上野公園絕對是不能錯過的賞櫻勝地之一。這個充滿浪漫氛圍的公園擁有超過1000棵櫻花樹,每年都吸引著無數遊客前來欣賞這片粉紅色的花海。

除了白天的賞櫻之外,上野公園還有一項令人神往的活動,那就是傍晚的點燈活動。當夜幕降臨時,整個公園被點亮,營造出一種浪漫的氛圍,讓遊客們可以在夜幕下欣賞到迷人的夜櫻。這種別具一格的賞櫻體驗絕對讓人流連忘返。

在櫻花季期間,上野公園也會舉辦不同的活動和表演,讓遊客可以感受到濃濃的日本文化氛圍。無論是品嚐傳統的日本料理、參加傳統音樂表演還是購買精美的手工藝品,都能讓遊客們深入體驗到日本的文化魅力。

此外,上野公園周邊還擁有眾多商店和小吃攤位,供遊客選擇,讓您可以在欣賞櫻花的同時品嚐道地的日本美食和購買紀念品。不僅如此,公園周邊還有許多著名的博物館和寺廟,讓遊客可以一次過體驗到日本的豐富文化遺產。

總括而言,2024年的東京櫻花季將會是一場難忘的賞櫻之旅。無論是白天還是夜晚,在上野公園中都能感受到浪漫氛圍,讓您與摯愛共度美好時光。趕快計劃您的行程,一起來感受日本春天的魅力吧!

下面是 Gemini 版本的程式碼 :

# news_generator_2.py
from google import genai
from google.genai.errors import APIError
from dotenv import dotenv_values

config=dotenv_values('.env') 
gemini_api_key=config.get('GEMINI_API_KEY')
client=genai.Client(api_key=gemini_api_key)

def ask_gemini(messages: str, model: str='gemini-2.5-flash') -> str:
    try:
        reply=client.models.generate_content(
            model=model, 
            contents=messages
            )
        return reply.text or ''
    except APIError as e:
        return e.message

prompt_role='''You are an assistant for journalists. 
Your task is to write articles, based on the FACTS that are given to you. 
You should respect the instructions: the TONE, the LENGTH, and the STYLE'''
# 你是記者的助理. 
# 你的任務是根據所提供的事實 (FACTS) 來撰寫一篇新聞稿.
# 文章需符合指定的語氣 (TONE), 長度 (LENGTH), 與風格 (STYLE). 

def assist_journalist(
    facts: list[str], 
    tone: str, 
    length_words: int, 
    style: str
    ) -> str:
    """根據傳入的事實、語氣, 長度與風格生成新聞稿"""
    facts_str=", ".join(facts)
    prompt=f'{prompt_role}\nFACTS: {facts_str}\nTONE: {tone}\nLENGTH: {length_words} words\nSTYLE: {style}'
    # Gemini 可直接接受字串作為 contents, 傳入串列可能會解析失敗
    return ask_gemini(prompt)

# 執行範例
if __name__ == "__main__":
    result=assist_journalist(
        facts=[
            '2026年東京櫻花預計在3月底滿開', 
            '上野公園是著名的賞櫻勝地,有超過1000棵櫻花樹', 
            '傍晚會有點燈活動,可以欣賞浪漫的夜櫻',
            '請使用繁體中文撰寫'  # 提示模型使用特定語言
            ],
        tone='浪漫且活潑', 
        length_words=300, 
        style='旅遊部落格文章'
        )
    print(result)

此 Gemini 版與上面 OpenAI 版主要不同處有二 :
  • 錯誤處理須匯入 google.genai.errors.APIError 類別.
  • Gemini SDK 可以接受字串作為 contents, 所以在 ask_gemini() 中傳入參數 messages 是字串, 而不是 OpenAI 中的串列 [{"role": "user", "content": prompt}], 否則 Gemini 的 SDK 可能會解析失敗 (視於版本相容性而定), 同時 assist_journalist() 內呼叫 ask_gemini() 也是直接傳 prompt 字串. 
注意, 此處使用新版 Gemini SDK : google-genai 而非舊版的 google-generativeai, 參考下面這篇底下的補充 :


執行結果如下 :

(myvenv) D:\python\test>python news_generator_2.py
## 2026東京櫻花前線速報!上野公園千株櫻花與夢幻夜櫻等你來!

嘿,各位櫻花迷們!是不是已經開始期待下一個粉紅色的春天了呢?告訴你一個振奮人心的好消息!2026年東京的櫻花季,預計將在**3月底**達到最美、最浪漫的滿開!準備好迎接這場不容錯過的春日盛典了嗎?

想抓住這份稍縱即逝的絕美風景,怎能錯過東京最經典、最有活力的賞櫻勝地——**上野公園**呢?這裡可是名符其實的「櫻花海」!園內種植了**超過1000棵**櫻花樹,想像一下,漫步在粉白色的花海隧道下,微風輕拂,花瓣如雪般飄落,光是想像就讓人心醉神迷。白天,公園裡總是充滿著歡聲笑語,野餐、散步、拍照,每一處都是生機勃勃的春日氣息。

但別以為太陽下山就結束了!上野公園的魅力在傍晚時分才真正達到高潮!入夜後,園內精心設計的**點燈活動**會將這些嬌嫩的櫻花裝扮得如夢似幻。一盞盞溫暖的光芒,輕柔地映照著粉白花朵,將白天的活潑氣氛轉化為一片極致浪漫的**夜櫻仙境**。和心愛的人手牽手,在微光中欣賞這份獨特的景色,絕對會是2026年春日最難忘的記憶,為你的東京之旅增添無限美好的浪漫色彩!

所以,親愛的朋友們,2026年3月底,趕快把你的行事曆空下來吧!東京上野公園的千株櫻花與夢幻夜櫻正等著你,一起來感受這場浪漫又活潑的春日盛典!別再猶豫了,現在就開始規劃你的東京櫻花之旅吧!

2025年10月12日 星期日

在 render.com 佈署 Python 網頁應用程式 (四)

在前一篇測試中我們已利用架設在 render.com 的仿 GCF 函式執行平台 serverless 當作 LINE Bot 的後端回應伺服器, 順利實作了一個 Echo bot, 它會傳回我們所說的話. 本篇旨在測試如何進一步讓後端回應程式串接 OpenAI, 變成一個 AI 聊天機器人. 

本系列之前的測試文章參考 : 


關於 OpenAI API 用法參考 :


為了能串接 OpenAI API, 我們已在前一篇測試中修改 requirements.txt 檔安裝 openai 套件, 因此在函式模組中可以直接匯入 openai 套件來使用. 


1. 設定環境變數 : 

由於在佈署 serverless 服務時忘了在 .env 檔案中添加 OpenAI API 的金鑰, 這可在儀錶板的 Environment 頁面的 Secret File 項目中按 Edit 鈕編輯即可 :




按右邊的檢視鈕 :




在彈出視窗中貼上要添加的 OPENAI_API_KEY , 按 Done 鈕存檔 :




按右邊的按鈕選擇 "Save, rebuild, and deploy" :




修改環境變數必須這樣才能確保有載入容器內. 


2. 撰寫後端回應程式串接 GPT 模型 : 

在前一篇測試中, Render 上的後端回應程式 linebot_echo.py 僅僅是取得使用者詢問後便傳回去, 如果要與 GPT 模型聊天, 就必須將詢問傳給 GPT, 取得模型回應後再傳回給使用者. 在 serverless 平台按 "新增函式" :




輸入如下程式碼 : 

# linebot_gpt.py
from flask import abort
from linebot import LineBotApi, WebhookHandler
from linebot.exceptions import InvalidSignatureError
from linebot.models import MessageEvent, TextMessage, TextSendMessage
import os
from openai import OpenAI

def main(request, **kwargs):
    # 取得主程式傳入的 LINE 與 OpenAI 金鑰權杖
    config=kwargs.get('config', {})
    secret=config.get('LINE_CHANNEL_SECRET')
    token=config.get('LINE_CHANNEL_ACCESS_TOKEN')
    openai_api_key=config.get('OPENAI_API_KEY', os.getenv('OPENAI_API_KEY'))

    # 檢查必要參數
    if not secret or not token or not openai_api_key:
        return {'error': 'Missing LINE or OpenAI credentials'}

    # 初始化 LINE API 與 Webhook Handler
    line_bot_api=LineBotApi(token)
    handler=WebhookHandler(secret)

    client=OpenAI(api_key=openai_api_key)

    # 註冊 MessageEvent + TextMessage 處理器
    @handler.add(MessageEvent, message=TextMessage)
    def handle_message(event):
        user_text=event.message.text  # 取得 LINE Bot 使用者詢問

        # 呼叫 OpenAI GPT 生成回應
        response=client.chat.completions.create(
            model='gpt-3.5-turbo',   # GPT 模型
            messages=[{'role': 'user', 'content': user_text}],
            max_tokens=300
            )
        reply_text=response.choices[0].message.content

        # 回傳給 LINE Bot 使用者
        line_bot_api.reply_message(
            event.reply_token,
            TextSendMessage(text=reply_text)
            )

    # 取得簽章與 request body
    signature=request.headers.get("X-Line-Signature", "")
    body=request.get_data(as_text=True)

    # 驗證簽章是否正確
    try:
        handler.handle(body, signature)
    except InvalidSignatureError:
        abort(400, "Invalid signature")

    return {"status": "ok"}




存檔後在函式列表頁上, 將滑鼠移到 linebot_gpt 模組的 "執行" 連結處, 按滑鼠右鍵點選 "複製連結網址" 即可取得此回應程式的 web hook 網址 :

# https://serverless-fdof.onrender.com/function/linebot_gpt



  
此網址無法直接執行, 必須經過驗證是從 LINE 伺服器發出的才會回應. 

然後登入 Line 開發者控制台將聊天機器人的 Webhook URL 改成這個網址經 Verify 成功即可, 作法參考前一篇測試 : 




這樣便能與 GPT 模型聊天了 :



2025年10月7日 星期二

OpenAI API 儲值 5 美元

昨天發現 OpenAI API 儲值餘額因為超過一年被歸零, 導致呼叫 API 失敗, 為了後續 LangChain 測試需要, 今天上網儲值 5 美元 (去年 10 美元似乎太多了, 用不完超過 9 美元被歸零哭哭, 這次保守一點, 五美元就好). 參考 :


首先在 Billing 頁面按 "Add to credit balance" 鈕 :






在 "Amount to add" 欄輸入 5 美元 :




加上 0.25 稅共 5.25 美元 : 



按其中的 "service credit term" 會顯示服務條款 : 


其中第 4 條就有寫道, 儲值使用期限為一年 (大部分人都不會仔細看甚麼條款) : 




再次檢視 Billing 頁面, 儲值餘額已顯示 5 美元了 :




5.25 美元折合台幣 159 元 (刷中國信託信用卡好處就是不論刷多少馬上會收到 LINE 訊息) :




這次儲值到期日為明年 10/7 日, 要設定行事曆, 每三個月提醒一次呵呵 (但接下來會用 LangChain 進行大量測試, 或許尚未到期就要再次儲值了). 

OpenAI API 儲值有一年使用期限

昨天晚上在 Colab 上調用 OpenAI API 分析財報資料時出現 Error code 419 錯誤 (insufficient_quota), 意思是我的儲值餘額已經見底了 :




RateLimitError: Error code: 429 - {'error': {'message': 'You exceeded your current quota, please check your plan and billing details. For more information on this error, read the docs: https://platform.openai.com/docs/guides/error-codes/api-errors.', 'type': 'insufficient_quota', 'param': None, 'code': 'insufficient_quota'}}

咦, 我 9 月底測試 LangChain 時還剩下 9.18 美元啊! 





原以為 API Key 被盜用, 但查詢 Usage 並未發現此狀況 : 



於是我到 OpenAI Help 頁面去詢問 AI 客服 : 

"Did the pay-as-you-go have expiration date?"

它的回答是肯定的 :

"For pay-as-you-go API credits, any purchased credits are valid for one year from the date of purchase. After one year, any unused credit will expire and is non-refundable. For example, if you paid $10 for credits on September 16, 2024, those credits would expire on September 16, 2025. If your credits are shown as expiring on October 1, 2025, that might reflect either a small timing discrepancy or a later purchase/adjustment—typically, credits expire exactly one year after purchase.

So yes, pay-as-you-go credits do have an expiration date: 12 months after you buy them."

早知道的話, 九月底之前就應該狂用給它花掉才對啊! 9 美元就這樣被沒收了. 

2025年6月27日 星期五

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日 星期四

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 平台 :