2026年4月15日 星期三

好站 : 工程師下班有約

最近上了幾堂林鼎淵老師的 Vibe coding 內訓課程, 覺得他是實力厚實備課非常認真的老師, 這從等候程式執行的幾分鐘, 他都有準備小知識與開發經驗談來墊檔與分享可見一斑. 他經營的 YT 頻道有非常多實用與最新的 AI 技術分享影片 (尤其是 Vibe coding), 值得記下來有空時學習 : 


2026年4月14日 星期二

(補記) 購買 Accupass AI 實戰工作坊

月初 (大約 4/6) 在 Accupass 買了一堂 Vibe coding 線上課程忘了記錄下來, 今天整理截圖才發現, 4/18 (六) 13:30~16:30 上課, 馬上列入行事曆以免錯過 :




我在 Hahow 也還有四門課買了尚未啟用學習, 第二季要來完成進度了. 

如何尋找靜音中的手機

前天 (週日) 爸坐阿泉伯的車去愛心會開會, 手機掉在車上未發覺, 回到家才發現手機不見了, 我透過定位發現手機在阿泉伯家, 但去他家找了三遍都沒找到, 撥電話都沒聽到響鈴, 我懷疑爸是否誤將手機關靜音, 於是問 Gemini 如何在手機靜音狀態下讓它發出聲音? 方法如下 :
  1. 在電腦或其他行動裝置的瀏覽器網址 google.com/android/find 
  2. 登入要尋找的手機的 Google 帳號
  3. 在左側面板或選單中點擊 「播放音效」(Play Sound)
我開啟自己的手機瀏覽器, 連線 google.com/android/find, 登入爸的 Google 帳戶後, 點選 "安全性與登入" :




點選尋找遺失的裝置 : 




點選要找尋的手機 OPPO A38 :




按 "播放音效" 就會持續播放 5 分鐘的響鈴了 :





最後是在阿泉伯媳婦提醒下在車子副駕椅子下找到手機, 雖然這個方法並未建功 (在車內就算響鈴除非走近也聽不到), 但如果要在家裡某個角落找尋亂放的手機就能派上用場了. 

2026年4月12日 星期日

2026 年第 14 周記事

時光飛逝, 思緒還駐留在大阪城門口初綻的櫻花, 怎麼一轉眼已來到四月中旬矣, 三周前還要穿長袖冬衣, 現在已換季來到夏天了, 三月真是一個天氣多變的月份. 

本周回頭抓了一下 LangChain 與 LLM 串接的進度, 其實只是想對借來的書有所交代而已, 目前重點還是 Vibe coding, 以及較專業些的 SDD, 這周末都在補課, 之前購買的線上課程在 3/28 就開課了, 但那時在忙掃墓, 所以沒跟上. 上完課覺得, 我其實都只開發小型專案而已, SDD 對我而言似乎大砲打小鳥. 不過或許以後用得著, 就學起來吧.  

今天早上爸去參加愛心會理監事開會, 中午回來吃過飯後發現手機不見了, 我用 Google 地圖定位發現手機在阿泉伯家, 但我騎車下去找, 現場撥電話也沒聽到響鈴, 猜測可能掉水裡, 但周邊水溝來回也沒找著. 傍晚忙完芭樂套袋, 騎機車再次去阿泉伯家, 剛好遇見他媳婦阿勤, 我說好奇怪, 明明定位在你家, 為何找不到? 她說會不會掉在車裡, 因為早上爸是搭阿泉伯車去開會, 打開車門果然掉在椅子下, 難怪撥電話都聽不到響鈴. 幸好找到了, 不然得買新手機, 且還要跑中華電信換 SIM 卡. 

2026年4月11日 星期六

LangChain 學習筆記 : 提示詞模板 (二)

上一篇測試是針對字串提示詞模板類別 PromptTemplate, 主要是用於單一句子的簡單提問, 功能類似於 f 字串, 可快速填充模板參數以得到完整的訊息字串. 本篇旨在測試需要更多上下文語境的對話提示模板類別 ChatPromptTemplate 之用法. 

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


本篇同樣會使用 OpenAI 與 Gemini API 來測試對話提示詞模板用法, 首先匯入所需之金鑰與外掛套件並預先建立模型物件 :  

(myvenv) D:\python\test>python   
Python 3.12.1 (tags/v3.12.1:2305ca5, Dec  7 2023, 22:03:25) [MSC v.1937 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> from langchain_openai import ChatOpenAI   
>>> from langchain_google_genai import ChatGoogleGenerativeAI  
>>> from dotenv import dotenv_values   
>>> config=dotenv_values('.env')   
>>> openai_api_key=config.get('OPENAI_API_KEY')   
>>> gemini_api_key=config.get('GEMINI_API_KEY')   
>>> gpt_model=ChatOpenAI(api_key=openai_api_key, model='gpt-3.5-turbo')     
>>> gemini_model=ChatGoogleGenerativeAI(api_key=gemini_api_key, model='gemini-2.5-flash')   

然後從 langchain_core 匯入 prompts 子模組下的 ChatPromptTemplate 類別 (因為之前只安裝 langchain_core, 沒有安裝大禮包 langchain) :

>>> from langchain_core.prompts import ChatPromptTemplate   

ChatPromptTemplate 類別的核心在於角色扮演, 它不像 PromptTemplate 那樣把所有內容塞成一個大字串, 而是將對話拆解成不同的角色訊息, 在 LangChain 的訊息物件中有三種角色 : 
  • System (系統) : 設定 AI 的人格, 背景, 規則 (例如 "你是一位 Python 專家")
  • Human (使用者) : 你對 AI 的提問
  • AI (助手) : AI 之前回覆過的內容 (用於提供對話脈絡)
呼叫 ChatPromptTemplate 類別的建構式 ChatPromptTemplate() 時, 要把對話內容依照角色拆分, 將其 system, human, 與 ai 三種角色的訊息組成元組串列傳入建構式, 傳回值為一個 ChatPromptTemplate 物件, 例如下面的翻譯對話模板 : 

>>> chat_template=ChatPromptTemplate.from_messages([
    ("system", "你是一位專業的 {style} 翻譯員,擅長將繁體中文翻譯成優雅的 {language}。"), 
    ("human", "請翻譯這段話:{text}"), 
    ]) 

此模板中嵌入了三個模板參數 : style, language, 與 text, 填充此三個參數可以呼叫 ChatPromptTemplate 物件的兩個方法 : 
  • format_message() : 傳入值為個別模板參數, 傳回值為訊息物件串列
  • invoke() : 傳入值為模板參數字典, 傳回值為 ChatPromptValue 物件
不論是哪一種傳回值, 都可以做為參數直接傳給模型物件的 invoke() 方法向 LLM 提交提示詞. 首先來測試使用 format_message() 來填充模板參數, 先準備要翻譯的文本 text : 

>>> text='''趁著午後的陽光灑進窗台,我決定暫時放下手中的電路板與程式碼,給自己泡一杯清茶,享受這難得的靜謐時光。生活不應該只有邏輯與迴圈,還要有隨處可見的驚喜。''' 

然後傳入 style, language, 與 text 參數來填充模板 :

>>> messages=chat_template.format_messages(style="出版業", language="英文", text=text)    
>>> type(messages)   
<class 'list'>   
>>> messages    
[SystemMessage(content='你是一位專業的 出版業 翻譯員,擅長將繁體中文翻譯成優雅的 英文。', additional_kwargs={}, response_metadata={}), HumanMessage(content='請翻譯這段話:趁著午後的陽光灑進窗台,我決定暫時放下手中的電路板與程式碼,給自己泡一杯清茶,享受這難得的靜謐時光。生活不應該只有邏輯與迴圈,還要有隨處可見的驚喜。', additional_kwargs={}, response_metadata={})]

可見 format_messages() 傳回一個訊息物件串列, 裡面有 SystemMessage 與 HumanMessage 等角色的訊息物件 (此對話尚未有 AIMessage 物件), 這樣就可以把填充完的訊息物件串列傳給模型的 invoke() 方法來生成回應了, 例如 GPT : 

>>> response=gpt_model.invoke(messages)   
>>> print(response.content)     
With the afternoon sunlight streaming in through the window, I decided to temporarily set aside the circuit boards and code in my hands, brew myself a cup of clear tea, and savor this rare moment of tranquility. Life shouldn't just be about logic and loops; it should also be filled with unexpected delights at every turn.

下面是串接 Gemini 的回應 :

>>> response=gemini_model.invoke(messages)   
>>> print(response.content)  
以下是幾種翻譯選項,從較為直接到更為優雅的風格:

**選項一 (較為直接,但仍優雅):**

> As the afternoon sun streamed onto the windowsill, I decided to temporarily set aside my circuit board and code. I would brew myself a cup of soothing tea and savor this rare moment of tranquility. Life shouldn't be solely about logic and loops; it should also be filled with readily found surprises.

**選項二 (更為優雅,帶有文學氣息):**

> Bathed in the afternoon sun gracing the windowsill, I chose to momentarily put down my circuit board and code. A cup of fine tea beckoned, promising a rare interlude of serenity. Life, after all, should not be confined to logic and loops, but should also embrace the unexpected delights found at every turn.

**選項三 (簡潔而富有詩意):**

> With the afternoon sun warming the windowsill, I paused from my circuit board and code. A soothing cup of tea, a rare moment of quietude – these were my new companions. For life, I believe, demands more than just logic and loops; it calls for the unexpected joys that lie around every corner.

---

**選擇建議:**

*   如果希望保持原句的結構和語氣,**選項一** 是個不錯的選擇。
*   如果希望加入更多文學色彩和更豐富的詞彙,讓譯文讀起來更具美感,**選項二** 會是最佳選擇。
*   如果偏好更精煉、更具哲思的表達,**選項三** 則能達到這個效果。

我個人最推薦**選項二**,它最能體現「優雅的出版業翻譯」的要求。

接下來改用 ChatPromptTemplate 物件的 invoke() 方法來填充, 這時要把模板參數打包成字典傳給模型物件的 invoke() 方法 : 

>>> messages=chat_template.invoke({
    'style': '科技業', 
    'language': '日文', 
    'text': '這支程式的執行效率非常高。'
    })
>>> type(messages)   
<class 'langchain_core.prompt_values.ChatPromptValue'>   
>>> messages   
ChatPromptValue(messages=[SystemMessage(content='你是一位專業的 科技業 翻譯員,擅長將繁體中文翻譯成優雅的 日文。', additional_kwargs={}, response_metadata={}), HumanMessage(content='請翻譯這段話:這支程式的執行效率非常高。', additional_kwargs={}, response_metadata={})])

可見傳回值是一個 ChatPromptValue 物件, 裡面包裹著 SystemMessage 與 HumanMessage 等角色的訊息物件, 可以直接把這個 ChatPromptValue 物件傳給模型的 invoke() 方法來生成回應, 例如 GPT : 

>>> response=gpt_model.invoke(messages)   
>>> print(response.content)     
このプログラムの実行効率は非常に高いです。

也可串接 Gemini :

>>> response=gemini_model.invoke(messages)   
>>> print(response.content)   
這支程式的執行效率非常高。
**このプログラムは実行効率が非常に高いです。**

或者,稍微更強調性能的話:
**このプログラムの実行効率は非常に優れています。**
*(This program's execution efficiency is excellent.)*

2026年4月10日 星期五

LangChain 學習筆記 : 提示詞模板 (一)

LangChain 的提示詞模板 (Prompt Templates) 功能利用預留變數將靜態文字轉化為動態指令, 可根據不同的傳入參數重複生成結構化的提示詞. LangChain 的 langchain_core.prompts 子模組提供兩種提示詞模板 :
  • 字串提示模板 (PromptTemplate 類別):
    用於單一句子的簡單提問
  • 對話提示模板 (ChatPromptTemplate 類別) :
    用於需要更多上下文語境之完整對話
本篇旨在測試字串提示模板 PromptTemplate 類別的用法. 

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


以下將使用 OpenAI 與 Gemini API 來測試字串提示模板用法, 首先匯入所需之金鑰與外掛套件並預先建立模型物件 :  

(myvenv) D:\python\test>python   
Python 3.12.1 (tags/v3.12.1:2305ca5, Dec  7 2023, 22:03:25) [MSC v.1937 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> from langchain_openai import ChatOpenAI   
>>> from langchain_google_genai import ChatGoogleGenerativeAI  
>>> from dotenv import dotenv_values   
>>> config=dotenv_values('.env')   
>>> openai_api_key=config.get('OPENAI_API_KEY')   
>>> gemini_api_key=config.get('GEMINI_API_KEY')   
>>> gpt_model=ChatOpenAI(api_key=openai_api_key, model='gpt-3.5-turbo')     
>>> gemini_model=ChatGoogleGenerativeAI(api_key=gemini_api_key, model='gemini-2.5-flash')   

由於之前在虛擬目錄下只安裝 langchain_core, 沒有安裝大禮包 langchain; 所以要從 langchain_core 匯入 prompts 子模組下的 PromptTemplate 類別 :

from langchain_core.prompts import PromptTemplate

>>> from langchain_core.prompts import PromptTemplate    

然後呼叫其建構式 PromptTemplate() 並傳入 template (字串) 與 input_variables (串列) 參數建立 PromptTemplate 物件, 其中 input_variables 中列舉了要傳入之模板參數, 例如 : 

prompt_template=PromptTemplate(
    template='用 300 個以內的字說明關於{topic}的知識',
    input_variables=['topic']  
    )

此 PromptTemplate 物件含有一個模板參數 topic. 

>>> prompt_template=PromptTemplate(
...     template='用 300 個以內的字說明關於{topic}的知識',   
...     input_variables=['topic']  
...     )  
>>> type(prompt_template)   
<class 'langchain_core.prompts.prompt.PromptTemplate'>
>>> prompt_template   
PromptTemplate(input_variables=['topic'], input_types={}, partial_variables={}, template='用 300 個以內的字說明關於{topic}的知識')

上面所建立的 PromptTemplate 物件只是一個模板結構而已, 還不能直接做為訊息物件傳給語言模型, 必須呼叫 PromptTemplate 物件的 format() 或 invoke() 方法傳入模板參數進行填充後才行, 這兩個方法的差異如下 : 
  • format() :
    單純的字串填充, 傳入值為單一字串, 傳回值是填充後的字串, 效果類似 f 字串的功能. 
  • invoke() :
    這是 LCEL (LangChain Expression Language) 的標準接口, 是為了配合 LCEL 的鏈式調用而設, 也是推薦的用法. 它的傳入值為模板參數字典, 傳回值為 PromptValue 物件, 它會根據後續接的是純文字模型還是對話模型自動轉成 StringPromptValue 或 ChatPromptValue 物件後傳回. 
總之, 如果是要除錯, 列印提示詞內容, 或串接非 LangChain 的自定義函數等場景, 可以呼叫 format() 以快速得到填充後的字串; 如果是要參與 LangChain 的鏈 (Chain) 運算, 則應該呼叫 invoke(). 

首先用 format() 方法來填充 :

>>> prompt_string=prompt_template.format(topic='疊加態')   
>>> type(prompt_string)    
<class 'str'>  
>>> prompt_string   
'用 300 個以內的字說明關於疊加態的知識'

可見 format() 傳回填充後的訊息字串, 這樣就可以傳給模型物件的 invoke() 讓模型來生成回應了, 例如 OpenAI 的 GPT :

>>> response=gpt_model.invoke(prompt_string)   
>>> print(response.content)  
疊加態是一種量子物理學中的概念,指的是當一個系統同時處於多個可能的狀態時,這些狀態之間可以相互叠加,而不會直接混合在一起。
在疊加態中,系統可以同時存在於不同的狀態中,直到進行測量時才會產生確定的結果。這也解釋了量子超密碼等量子現象的奇異性。
疊加態是量子力學中的一個核心概念,也是量子計算和量子通信等領域的基礎。通過利用疊加態,科學家們可以設計出更快速和更強大的計算機系統,以及更加安全和高效的通訊技術。
總的來說,疊加態是一個深奧而又神秘的概念,它挑戰了我們對現實世界的直覺和理解,也為我們揭示了量子世界的獨特之處。

或者谷歌的 Gemini : 

>>> response=gemini_model.invoke(prompt_string)   
>>> print(response.content)   
疊加態是量子力學中的一個基本概念,指微觀粒子在被測量前,可以同時處於多個可能的狀態。例如,一個電子可以同時向上和向下自旋,或同時在多個位置。只有當我們進行觀測或測量時,疊加態才會「坍縮」,粒子隨機選擇其中一個狀態顯現出來。在未觀測前,它並非處於某個確定的狀態,而是所有可能性的疊加。

其次, 改為呼叫 invoke() 方法, 這時傳入參數是模板參數字典 : 

>>> prompt_value=prompt_template.invoke({'topic': '普郎克常數'})   
>>> type(prompt_value)   
<class 'langchain_core.prompt_values.StringPromptValue'>  
>>> prompt_value   
StringPromptValue(text='用 300 個以內的字說明關於普郎克常數的知識')    

由於模板是純文字模型, 所以傳回值為 StringPromptValue 物件, 可以直接將此 PromptValue 物件傳給模型來生成回應, 例如 GPT :

>>> response=gpt_model.invoke(prompt_value)   
>>> print(response.content)     
普朗克常數是物理學中的一個重要常數,通常用符號h表示。它的數值約為6.626×10^-34 J·s。普朗克常數被廣泛應用於量子力學和粒子物理學中,尤其是在描述微觀世界中微小粒子的運動和行為時。根據普朗克常數,能量和頻率之間存在著一個固定的關係,即E=hf,其中E是能量,h是普朗克常數,f是頻率。普朗克常數還與黑體輻射和光子的能量密度等現象密切相關。因此,普朗克常數在物理學中具有重要的地位,對於我們理解微觀世界的運作方式至關重要。

或者 Gemini 模型 : 

>>> response=gemini_model.invoke(prompt_value)   
>>> print(response.content)  
普朗克常數(h)是一個基本物理常數。它揭示了能量的量子化現象,即能量不是連續的,而是以離散的「量子」形式存在。它將光子的能量E與其頻率ν聯繫起來,公式為E=hν。普朗克常數是量子力學的基石,描述了微觀世界中能量的最小作用量單位。

這就是使用 LangChain 的好處, 不論是串接哪一個 LLM, 呼叫介面都相同. 

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月底,趕快把你的行事曆空下來吧!東京上野公園的千株櫻花與夢幻夜櫻正等著你,一起來感受這場浪漫又活潑的春日盛典!別再猶豫了,現在就開始規劃你的東京櫻花之旅吧!

2026年4月7日 星期二

好書 : 歸剛 Vibe Coding

今天在博客來找到這本 Vibe coding 的電子書 :


我用 HyRead 查詢市圖未進此書, 但台灣雲端書庫有, 借期 14 天, 由於篇幅不大, 我今天就看完了, 摘要心得如下 :
  • 創作時只要專注在 "我要完成甚麼", 而不是去糾結 "我要學習甚麼", 碰到問題才查才問, 這樣完成率就會很高. 這種狀態下, 沒有刻意要學, 卻學得超快. 
  • 對企業來說, 員工永遠都是可有可無的開銷. 
  • 創業的原理很簡單 : 發現需求 -> 發揮想像力與觀察力 -> 勇敢去實踐
  • 創業=解決問題+創造價值+實際賺錢.
  • 上班族賣的是時間, 創業者賣的是點子變出來的資產.
  • AI 時代的成功不再是靠背景, 學歷, 資源, 而是靠有想法+會動手. 
  • Vibe coding 的核心觀念 : 先創造感覺, 再修細節>
  • Vibe coding 步驟 :
    • 快速生成原型 : 利用 AI 工具+現成框架 (Next.js, Tailwind, Supabase, ...)
    • 即時發布 (Vercel 等)
    • 社群平台 (Discord, Threads, ...) 回饋驅動迭代
  • Vibe coding 是從寫得正確轉向為先做出感覺. 市場變化很快, 與其寫規格書, 不如先丟作品上去看市場反應. 
  • 不論是做網站還是 App, 開發任何產品前首先要問 : 誰是使用者? 不要去想 "他們需要甚麼功能" (feature-to-be-shipped), 而是要想 "他們想完成甚麼事" (job-to-be-done). 總之, 不要急著做, 先把使用人的問題搞懂. 
  • Rig : 一套能把靈感迅速變成現實的工具組合 (AI 工具 + 開發框架 + 雲端佈署). 
  • 開發框架技術堆疊 :
    • Next.js : 最快速的 React 框架
    • Tailwind : 直接用 class 寫樣式
    • Supabase / PlanetScal : 資料庫即服務
    • Bun / Vite : 快速的 Javascript 框架
  • 網頁工具技術堆疊 :
    • Figma : 網頁 UI 外觀
    • Wenflow / Proto.io : 互動網頁工具
    • Vercel : 一鍵完成佈署
  • Vibe coding 的關鍵不是要懂很多技術, 而是要選對工具與問對問題. 
  • 向 AI 提問時最大的忌諱是一次丟出一個又大又模糊的問題, 否則它會亂猜你的需求, 應該要這樣問 : "我想做一個 xxx, 裡面有 xxx, 先問我問題跟我討論". 在動手開發之前, 應該先進行討論, 而不是直接行動. 問 AI 有甚麼問題要問你其實就是讓 AI 幫你找出完成此專案需要考慮的事項, 讓你得到一連串切中核心的問題. 
  • 與 AI 溝通的要訣 :
    • 可在專案目標後面加上這段 : "請針對我提出的目標和方向提出最詳細的問題, 不要當好好先生, 別甚麼都同意, 要質疑我選的方向與決定, 永遠不要自行假設", 可避免 AI 亂編內容, 並能主動問你問題以釐清細節.
    • 請 AI 一次做一個步驟, 否則它會一股腦兒把全部解決方案爆出來, 如果方向錯誤要浪費較多時間導正. 可加上這段提示 : "請一步一步來, 不要一次全部給我". 
  • 隨著來回討論越長, AI 的反應會越慢效率變差, 可用下列提示詞來做一個總結與繼承動作 : "請整理目前我們所建立的重要上下文, 包含所有決策, 定義, 專案目標, 技術設置, 程式結構, 使用工具, 命名規則, 文字語氣, 以及任何對此專案重要的資訊, 請整理得清楚明瞭, 方便我複製貼上". 然後複製下來, 開啟一個新對話貼上, 同時貼上專案資料夾的專案結構樹 (用 tree 指令). 然後加上這句 "請在開始對話前, 告訴我你想要我貼上哪些檔案的內容?", 將第一個檔案內容貼給它時, 加上這句 "這是第一個檔案, 只要告訴我你收到了即可, 不用進行分析,  然後告訴我你要看的下一個檔案, 等全部貼完再分析."  (我覺得這個做法很不錯)

2026年4月6日 星期一

Hahow 購買黑木太太日語課

最近在 fb 看到黑木太太日語課在募資, 今天是 88 折優惠最後一天, 想說退休前的這幾年會很常去日本玩, 最好把日語練好一點, 就購買了此課程 :





最近開車都在聽 Yumi 老師的日文課, 聽久了不知不覺就背起來了.