跳至主要内容

中島美雪《時代》

· 閱讀時間約 4 分鐘

《時代》

中島美雪的代表作《時代》,是在 1975 年那一年,她連續闖過兩個比賽的一首歌。

1975 年 10 月:Popcon

10 月 12 日,第 10 屆 Popcon(ポピュラーソングコンテスト)在靜岡縣つま恋舉辦。

那一屆收到了 12,000 首 應募曲,中島美雪的〈時代〉拿下了最高獎 グランプリ。也因為這個獎,她得到了代表日本參加 11 月世界歌謠祭的資格。

1975 年 11 月:世界歌謠祭

11 月 16 日,「第 6 屆世界歌謠祭」在日本武道館舉辦。〈時代〉再次拿下了最大獎 Grand Prix(グランプリ)。

那一屆的主持人,是日本傳奇歌手坂本九,以及當時已經紅遍亞洲的翁倩玉

下面這段珍貴的影片中,約 1 分 56 秒處,收錄了當年在世界歌謠祭的影像,你可以清楚地聽到坂本九與翁倩玉主持的聲音。


坂本九:「Entry number 30, 日本, 中島みゆき, "時代".」

翁倩玉:「Entry number 30, Japan, "Time Goes Around".」

伴奏的故事

〈時代〉在舞台上原本是有管弦樂團伴奏的版本。但拿下 Grand Prix 之後的 得獎者加演(安可),中島美雪走上台前,對著指揮耳語了幾句。接著,整個樂團安靜下來,她抱著吉他,只用一把吉他自彈自唱了一遍〈時代〉。

這個決定來自於她的伯樂、YAMAHA 音樂振興會的理事長 川上源一 對她說過的一段話:

「あなたはすごい詞を書く。将来、詞で勝負するようなアーティストに育って欲しい。できれば大音量をバックにするよりも、ギター一本で歌った方が、あなたの詞が人々に伝わる」

「妳寫的詞非常出色。希望將來你能成長為一位以歌詞實力取勝的藝術家。如果可以的話,與其背負著巨大的音量(伴奏),不如只用一把吉他自彈自唱,你的歌詞反而更能傳達進人們的心裡。」(AI 翻譯)

從那之後,中島美雪在每一張專輯的工作人員名單裡,都會寫上一行 「DAD 川上源一」,以此致敬這位恩人。

關於父親

中島美雪在出道單曲〈薊花姑娘的搖籃曲〉發行前一週(1975 年 9 月 16 日),父親就因腦溢血倒下、昏迷不醒。

10 月的 Popcon、11 月的世界歌謠祭,她其實都是從父親昏迷的病房趕到會場上台的。父親後來於 1976 年 1 月離世,始終沒能聽到女兒奪冠的消息。

世界歌謠祭頒給她的 5,000 美元獎金,後來拿去當作父親的葬儀費用。

1993 年的彩蛋

中島美雪在 1993 年重新錄製了《時代》,歌曲的開頭,她取樣了在世界歌謠祭裡面,主持人坂本九翁倩玉的介紹詞,以及歌曲開頭的一小段演唱。

翁倩玉:「Entry number 30, Japan, compose and sung by Miyuki Nakajima, the title of the song: Time Goes Around.」

(這聲音也太美了吧!!)

備註

我在「這篇貼文」中提到我前陣子跑去看了《陽光女子合唱團》,才發現其實我以前經常聽到這位國民阿嬤翁倩玉的聲音。

至於坂本九的話,我最喜歡的歌曲是《見上げてごらん夜の星を》。

參考資料

收集部落格 RSS 網址列表

· 閱讀時間約 2 分鐘

最近「部落格問題挑戰」這個問答挑戰突然很多人響應。

想說可以收集一下大家的答案,就像 Ava 的「bear blog question challenge」一樣!

這樣的話,我就需要收集一下大家的 RSS Feed 了,在這邊記錄一下做法。

雖然都是 AI 在做。


目標

BlogBlog 同樂會 頁面上的所有部落格整理成一份可匯入閱讀器的 OPML 清單

1. 取得部落格網址列表

  1. 在 BlogBlog 同樂會 頁面爬取所有連結。
  2. 去除重複網址,整理並輸出成 txt 檔案。

2. 將網址列表轉成 RSS Feed

  1. 先比對既有的 OPML(原本就訂閱過的就不用找了)
  2. 用平台規則猜 RSS 位置,用 cURL 去試試看,平台型部落格通常都會直接成功。
  3. 抓首頁 HTML,找 link rel="alternate",有的話通常就直接是 RSS 網址了。
  4. 測試常見路徑(/feed/rss/index.xml/atom.xml 等等)
  5. 輸出成 rss_feeds.csv:每個站的 RSS、來源、狀態

這時候大概能收集到九成的 RSS Feed 了,剩下的就手動稍微找一下,很快就處理完了!

最後輸出成 url.opml.xml,然後在我的 FreshRSS 閱讀器開一個帳號,把這份清單匯入進去。

之後只要上去搜尋關鍵字「部落格問題挑戰」就可以了!

e89295

· 閱讀時間約 1 分鐘

TIL,《我的部落格》的網站名稱「e89295」,是用三個十六進位的 UTF-8 位元組來表示的:0xE80x920x95

new TextDecoder().decode(new Uint8Array([0xE8, 0x92, 0x95]))

如何用 Telegram 傳送通知給自己

· 閱讀時間約 2 分鐘

前言

前幾天看到 Eddie 在這篇文章中提到,戒了 IG 後「會想一直開自己網站刷留言,看看有沒有新留言!」。

其實我的這套「留言系統」有接「通知功能」耶!只要有人留言,就會觸發 Telegram 的機器人通知我,這樣我就能快快審核,不用定時一直上去檢查。

不過,更簡單的方法應該是:Google Apps Script 內建直接用 Gmail 發信,這樣還能順便備份留言,一舉兩得,簡單又好用!(但我還沒試過,講得我自己都想用了)

正文

有時候會希望程式跑完某件事之後,自動發個通知給自己,像是排程結束、伺服器異常、或是有人填了表單之類的。

這件事其實用 Telegram 來做超簡單,只要申請一個 BOT、拿到 Chat ID,然後用 POST 打一下 Telegram 的 API 就搞定了!

BTW,其實我早期一直都是用 Line Notify 來做通知,但是 Line 又複雜又難用,而且 2025 年 Notify 功能就收掉了,於是就改成用 Telegram 來做通知了。

筆記

I'm Marcus 的閱讀軌跡(推薦系統)

· 閱讀時間約 2 分鐘

今天看到 I'm Marcus 的文章:「来玩玩新开发的小游戏吧」。

Marcus 在自己的部落格實作了一套基於「閱讀軌跡」的文章推薦系統,這個設計還滿有意思的。

我觀察到的一些東西:

  1. 文章向量壓縮到 64 維,整份資料不到 200KB,載入和計算速度都很快。
  2. 使用者記錄存在瀏覽器的 Local Storage,完全不需要後端,隱私性高又即時。
  3. 文章不僅能標記「喜歡」,還能標記「不喜歡」,系統會同時考慮兩種訊號。
  4. 根據「已反饋文章」計算出使用者向量,並對「未反饋文章」計算相似度,決定推薦哪篇文章,也不會重複推薦。
  5. 每次根據當下的狀態計算推薦,並且記錄下來,形成一條固定的閱讀軌跡。

我推測他的運作原理大概是:

  1. 使用者向量 = AVG(喜歡文章向量) - AVG(不喜歡文章向量)
  2. 計算所有的 Cosine Similarity(使用者向量, 未反饋文章)
  3. 取 Top 1 作為推薦

幾個吹毛求疵的小缺點:

  • 只有推薦一篇文章的話,我自己會覺得有點「被牽著走」的感覺,選擇有點太少了。(不過這樣才是「軌跡」,所以也不是缺點,只是一種取捨)
  • 按「換點口味」會被視為負反饋,但這不一定代表我對該主題不感興趣,可能只是對該文章不感興趣而已。
  • 要讓使用者評分實在是有點難度,但這確實是比較尊重使用者的做法。(通常會用隱性一點的特徵)

用 FFMPEG 製作 Nightcore 風格音樂

· 閱讀時間約 1 分鐘

今天跟 GPT 學到了用 FFMPEG 製作 Nightcore 風格音樂的方法。

Nightcore 的核心特徵是「音調變高、速度變快」,聽起來像花栗鼠在唱歌的那種感覺。

指令

ffmpeg -i input.mp3 -af "asetrate=44100*1.35,aresample=44100" output.mp3
  • asetrate=44100*1.35:改變音訊的播放速率,將採樣率強制改為原來的 1.35 倍,音調與速度都會同步提高。

  • aresample=44100:將採樣率重採樣回標準的 44100Hz,確保檔案在所有設備上都能正常播放。

微調速度

  • atempo=1.05:純速度濾波器,不影響音調,用來微調最終節奏。

例如可以這樣搭配:

ffmpeg -i input.mp3 -af "asetrate=44100*1.3,aresample=44100,atempo=1.05" output.mp3

用 Embedding 做「相關文章」推薦

· 閱讀時間約 2 分鐘

最近幫部落格做了一個「相關文章」功能,用 AI 的語義向量(Embedding)來計算文章之間的相似度。

原文:《DIY 系列:來做個「相關文章」功能


原理

三個步驟:

  1. 把文章餵給 Embedding 模型,得到一組向量(一串浮點數)。這組向量代表文章的「語意」,語意越接近的文章,向量在空間中的距離也越近。

  2. 計算餘弦相似度,也就是兩個向量之間的夾角,越接近 1 代表越相似。

  3. 排序取前 K 名,就是「相關文章」了。


兩種 Embedding 方案

方案一:Gemini API(免費)

Google AI Studio 申請 API Key,呼叫 gemini-embedding-001 模型。

免費方案有速率限制,我的做法是每篇截取前 2000 字,每隔兩秒呼叫一次。

方案二:BGE-M3 本地端(也免費)

Ollama 在本機跑 BGE-M3,CPU 就能跑,完全離線。

ollama pull bge-m3
pip install ollama
import ollama

res = ollama.embeddings(model="bge-m3", prompt="文章內容...")
vector = res["embedding"]

快取機制

每次重新計算 Embedding 很耗時,所以用 Hash(標題 + 內文) 來判斷文章是否有改變,沒變就直接讀快取。

相似度計算目前是全部重跑(讓新舊文章可以互相連結),一百多篇大概三秒,還可以接受。


完整流程

  1. 載入快取,用 Hash 比對找出需要更新的文章。
  2. 清除已刪除文章的舊快取。
  3. 對需要更新的文章重新 Embedding,存入快取。
  4. 計算所有文章兩兩之間的相似度,排序取前 K 名。
  5. 輸出 related.json 或直接生成靜態 HTML。

如何偵測檔案變動

· 閱讀時間約 2 分鐘

前言

我自己在目前部落格上有三種實際應用場景:

  1. Preview 網頁自動 reload:這個場景是高頻檢查,例如每秒看一次檔案有沒有變。就算偶爾誤判,最多只是多 reload 一次,所以我用檔案修改時間來判斷。這邊我用 Polling 的方式來做,雖然 I/O 比較重一點,但實現起來簡單,也不用去安裝其他套件。

  2. Build 建立靜態網站:這邊的重點是正確性,要精準知道內容是否真的改變,避免不必要重建或漏建,所以我會直接比對全文內容,也就是比對「渲染後的文章」與「輸出資料夾的文章」。

  3. 檔案變動後要打 API:打 API 通常有副作用,不能亂觸發。這時候不一定要知道改了哪裡,只要知道內容真的不同即可,所以我會把 Hash 存起來,如果有比對到 Hash 變更再去打 API。這種做法很優雅,不需要多留一份原始檔案,只要保留原檔案的 Hash 值就好了。

三種方法的差異

方法核心概念速度準確度代價
時間戳比對檔案最後修改時間很快可能會誤判
全文比對逐字比較內容需要讀兩份完整檔案
Hash 比對比較最終內容 Hash需額外保存 Hash 值

1. 比較檔案時間

只要看檔案最後修改時間有沒有變,就能快速判斷「可能有變更」。

  • 優點:極快、實作簡單,適合高頻率輪詢。
  • 缺點:有誤判機會,像是改了 metadata、或某些同步工具重寫時間。

2. 全文比對

直接把新舊檔案內容拿來比,這是最準確的方法。

  • 優點:只要內容一樣,就一定判定「沒變」。
  • 缺點:每次都要讀檔和比對,檔案越大越花時間。

3. Hash 比對

做法是先計算檔案 Hash,把它存起來,下次再算一次做比對。

  • 優點:不需要知道改了哪裡,只要知道內容是否不同,速度與儲存成本都不錯。
  • 缺點:仍然要讀取內容計算 Hash;另外需要管理一份本地 Hash 資料。

實務上,若是有副作用的操作(例如打 API、觸發部署、通知、計費),Hash 很適合。

如何幫網站新增一個 Theme

· 閱讀時間約 2 分鐘

前言

今天想嘗試幫網站做一個 theme。

以前我也曾經做過幾次修改,比如說「聖誕節主題」、「抹茶主題」等等。

原本我都是直接去修改 styles.css,後來發現這樣要做「期間限定」的 Theme 不是很方便。

所以我就把一些常見的顏色改成參數,可以直接用參數改變顏色,直接幫網站換 Theme。但有時候是連結構都會改變,這時候只改變數就不太夠用了(搞得太複雜也不好維護)。

所以呢,以下整理介紹三種常見的 Theme 做法:

1. CSS 變數切換

先把顏色、字體、間距等抽成 CSS 變數(Custom Properties),切換 Theme 時只要替換一組變數值。

/* 預設主題 */
:root {
--color-bg: #fff;
--color-text: #333;
}

/* 另一個 Theme */
:root[data-theme="retro"] {
--color-bg: #f2f2f2;
--color-text: #111;
}
  • 優點: 最輕量,只需維護一份 CSS。
  • 缺點: 只能改到已經參數化的屬性。像 box-shadowborder-radiusorder 這類結構型樣式,通常不夠用。

2. 完整替換整份 CSS

直接維護兩份完整 CSS(例如 default.csstheme.css),切換時整份替換。

  • 優點: 兩份 CSS 完全獨立,不需要考慮覆蓋衝突。
  • 缺點: 維護成本最高,共用元件的修改要同步兩份。

3. CSS 覆蓋(Override)

保留原本的 styles.css,再額外新增一份 theme.css,只覆蓋需要改動的規則。

.post-card {
background: #f2f2f2;
border-radius: 0;
}
  • 優點: 可以覆蓋任意屬性,不受限於變數;原本 CSS 幾乎不用動,載入即套用、移除即還原。
  • 缺點: 需要注意 CSS 優先權(specificity)與載入順序,必要時要調整選擇器權重。

實作上,只要在 HTML 的 <head> 多載入一行:

<link rel="stylesheet" href="/assets/css/styles.css">
<link rel="stylesheet" href="/assets/css/theme.css"> <!-- 加這行 -->

theme.css 直接用相同選擇器覆蓋 styles.css。由於它在後面載入,在相同優先權下會覆蓋前者(CSS cascade)。

如果要做即時切換

可以用 JS 搭配 body class 做 runtime 切換:

document.body.classList.toggle('theme-alt');

這時候,Theme 規則就要加上 body.theme-alt 前綴:

body.theme-alt .post-card { ... }
body.theme-alt .post-title { ... }

如果你要的是「快速上線、可恢復、又能改結構樣式」,通常 CSS 覆蓋會是最平衡的做法。

CSS 圖層魔術

· 閱讀時間約 2 分鐘

今天做了一個很白爛的圖層魔術

按下按鈕後,帽子先往下蓋住,再掀開,裡面會冒出兔子、青蛙,或者乾脆什麼都沒有。

說明

  • position: relative:讓舞台變成絕對定位元素的參考點。
  • position: absolute:把每張圖放到指定座標。
  • z-index:控制遮擋順序。帽子要蓋住角色,所以層級要最高。
  • transition:當 top 被改變時,自動產生移動動畫,做出「蓋住 -> 掀開」的節奏。
  • display: none 雖然簡單,但不能直接做淡入淡出;如果需要更順的切換,可以改用 opacity 搭配 visibility
  • 舞台尺寸最好固定,不然不同圖片尺寸可能會讓圖層位置跑掉。

程式碼

注意

圖片請自備~(或者去文章內右鍵下載)

<div class="magic-stage">
<style>
.magic-stage { position: relative; width: 300px; height: 400px; margin: 0px auto; text-align: center; }
.magic-stage img { position: absolute; transition: top 0.6s ease-in-out; }
.magic-stage .magic-table { width: 280px; left: 10px; top: 250px; z-index: 1; }
.magic-stage .magic-hat { width: 160px; left: 70px; top: 0; z-index: 3; }
.magic-stage .magic-rabbit { width: 80px; left: 110px; top: 170px; z-index: 2; display: none; }
.magic-stage .magic-frog { width: 80px; left: 110px; top: 195px; z-index: 2; display: none; }
.magic-stage .active { display: block; }
.magic-stage button { margin-top: 340px; padding: 10px 30px; cursor: pointer; border: 2px solid #6200ee; border-radius: 4px; background: #6200ee; color: white; font-size: 16px; font-weight: bold; transition: 0.2s; }
.magic-stage button:disabled { background: #ccc; border-color: #ccc; cursor: not-allowed; }
</style>

<img src="table.png" class="magic-table" alt="magic-table">
<img src="hat.png" class="magic-hat" alt="magic-hat">
<img src="rabbit.png" class="magic-rabbit" alt="magic-rabbit">
<img src="frog.png" class="magic-frog" alt="magic-frog">
<button class="magic-button" onclick="performMagic()">Magic</button>

<script>
let magicStep = 0;

function performMagic() {
const magicStage = document.querySelector('.magic-stage');
const magicHat = magicStage.querySelector('.magic-hat');
const magicRabbit = magicStage.querySelector('.magic-rabbit');
const magicFrog = magicStage.querySelector('.magic-frog');
const magicButton = magicStage.querySelector('.magic-button');

magicButton.disabled = true;
magicHat.style.top = "140px";

setTimeout(() => {
magicRabbit.classList.remove('active');
magicFrog.classList.remove('active');

if (magicStep === 0) {
magicRabbit.classList.add('active');
} else if (magicStep === 1) {
magicFrog.classList.add('active');
}

magicStep = (magicStep + 1) % 3;

magicHat.style.top = "0px";
setTimeout(() => magicButton.disabled = false, 600);
}, 1250);
}
</script>
</div>