高效能音檔轉錄工具:結合FFmpeg與OpenAI Whisper API

更新 發佈閱讀 23 分鐘

前一版本的不夠好用,然後好像也有點危險。所以再來修改一下
這次把 key 給拔出來了,先做 export OpenAI API 的 key
直接注入到系統的環境變數中

環境變數注入指令

macOS, zsh

echo 'export OPENAI_API_KEY="your_openai_api_key"' >> ~/.zshrc
source ~/.zshrc

Linux, bash

echo 'export OPENAI_API_KEY="your_openai_api_key"' >> ~/.bashrc
source ~/.bashrc

Windows PowerShell

# 只在目前視窗有效
$env:OPENAI_API_KEY = "your_openai_api_key"
python your_script.py

# 永久
setx OPENAI_API_KEY "your_openai_api_key"

程式碼

import os
import re
import json
import math
import tempfile
import subprocess
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor, as_completed
from typing import List, Tuple, Optional

from openai import OpenAI

# =============== 可調參數 ===============
INPUT_FILE = "250918_1704.mp3"
MODEL_NAME = "gpt-4o-mini-transcribe" # 或 "gpt-4o-transcribe"
LANGUAGE = "zh"

MAX_PART_MB = 20
MAX_MODEL_DURATION_SEC = 1400
MAX_SAFE_DURATION_SEC = 1300

TARGET_CHUNK_SEC = None
SILENCE_MIN_LEN = 0.6
SILENCE_THRESH_DB = -35
SILENCE_SEARCH_WINDOW = 3.5

# 轉成輕量母檔參數
LIGHT_SR = 16000 # 16kHz
LIGHT_BR = "64k" # 48~64k 都可
WORKERS = 4 # 3~5 之間視頻寬/速率調整
# =======================================

def sh(cmd: List[str]) -> subprocess.CompletedProcess:
return subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=True)

def ffprobe_json(path: Path) -> dict:
out = sh(["ffprobe","-v","error","-show_format","-show_streams","-print_format","json",str(path)]).stdout
return json.loads(out)

def get_duration(meta: dict) -> float:
if "format" in meta and "duration" in meta["format"]:
return float(meta["format"]["duration"])
for s in meta.get("streams", []):
if "duration" in s:
return float(s["duration"])
raise RuntimeError("no duration")

def get_bitrate_bps(meta: dict) -> Optional[float]:
br = meta.get("format", {}).get("bit_rate")
if br:
try: return float(br)
except: pass
for s in meta.get("streams", []):
if s.get("codec_type")=="audio" and "bit_rate" in s:
try: return float(s["bit_rate"])
except: pass
return None

def estimate_target_chunk_sec_by_size(path: Path, max_mb: int) -> int:
meta = ffprobe_json(path)
dur = get_duration(meta)
size_mb = path.stat().st_size / (1024*1024)
if size_mb <= max_mb: # 不切
return math.ceil(dur)
br = get_bitrate_bps(meta)
if not br:
mbps = size_mb / dur
return max(30, int((max_mb * 0.85) / mbps))
target_bits = max_mb * 1024 * 1024 * 8 * 0.85
sec = target_bits / br
return max(30, int(sec))

def parse_silence_points(path: Path) -> List[Tuple[float,float]]:
# 僅分析一次全檔
p = subprocess.run([
"ffmpeg","-i",str(path),
"-af",f"silencedetect=noise={SILENCE_THRESH_DB}dB:d={SILENCE_MIN_LEN}",
"-f","null","-"
], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
stderr = p.stderr
starts = [float(x) for x in re.findall(r"silence_start:\s*([0-9.]+)", stderr)]
ends = [float(x) for x in re.findall(r"silence_end:\s*([0-9.]+)", stderr)]
pairs=[]
si=ei=0
while si<len(starts) and ei<len(ends):
if ends[ei] > starts[si]:
pairs.append((starts[si], ends[ei]))
si+=1; ei+=1
else:
ei+=1
return pairs

def find_near_silence(pairs: List[Tuple[float,float]], target: float, window: float)->Optional[float]:
low, high = target-window, target+window
cand=[]
for s,e in pairs:
if low<=s<=high: cand.append(s)
if low<=e<=high: cand.append(e)
return min(cand, key=lambda x:abs(x-target)) if cand else None

def plan_boundaries(src: Path) -> List[float]:
"""回傳切點清單(秒),不含 0,含檔尾;供 segment_times 使用。"""
meta = ffprobe_json(src)
total = get_duration(meta)
br = get_bitrate_bps(meta)

target = TARGET_CHUNK_SEC or estimate_target_chunk_sec_by_size(src, MAX_PART_MB)
target = min(target, MAX_SAFE_DURATION_SEC)

# 不需切
size_mb = src.stat().st_size / (1024*1024)
if size_mb <= MAX_PART_MB and total <= MAX_SAFE_DURATION_SEC:
return [total]

sil = parse_silence_points(src)
cuts=[]
cur=0.0
while cur<total:
ideal = min(total, cur + target)
# 以位元率預估,過大就縮
if br:
while ((ideal-cur)*br/8/1024/1024) > MAX_PART_MB and (ideal-cur)>60:
ideal -= 15
cut = find_near_silence(sil, ideal, SILENCE_SEARCH_WINDOW) or ideal
if cut - cur < 10: # 避免太短
cut = min(total, cur + 10)
cuts.append(cut)
if cut >= total: break
cur = cut
# 再保險:任何一段 > MAX_SAFE_DURATION_SEC 的,再二次細分均切
final=[]
prev=0.0
for c in cuts:
seg = c - prev
if seg <= MAX_SAFE_DURATION_SEC:
final.append(c)
else:
parts = math.ceil(seg / MAX_SAFE_DURATION_SEC)
step = seg / parts
for i in range(1, parts+1):
final.append(min(prev + i*step, c))
prev = c
# 去重/排序
uniq=sorted(set(round(x,3) for x in final if x>0))
if uniq[-1] < total: uniq[-1]=total
return uniq

def to_light_master(src: Path, dst: Path):
# 轉 mono/16k 低碼率,後面切檔與上傳更快
sh([
"ffmpeg","-y","-i",str(src),
"-ac","1","-ar",str(LIGHT_SR),
"-vn","-c:a","libmp3lame","-b:a",LIGHT_BR,
str(dst)
])

def split_once(src: Path, out_dir: Path, boundaries: List[float]) -> List[Path]:
out_dir.mkdir(parents=True, exist_ok=True)
# segment_times 需為逗號分隔秒數(不含 0
times = ",".join(f"{t:.3f}" for t in boundaries[:-1]) if len(boundaries)>1 else ""
pattern = str(out_dir / "part_%03d.mp3")
cmd = ["ffmpeg","-y","-i",str(src),"-c","copy","-f","segment"]
if times:
cmd += ["-segment_times", times]
cmd += [pattern]
sh(cmd)
return sorted(out_dir.glob("part_*.mp3"))

def transcribe_one(client: OpenAI, path: Path) -> str:
# 簡單重試
for i in range(5):
try:
with open(path, "rb") as f:
resp = client.audio.transcriptions.create(
file=f, model=MODEL_NAME, language=LANGUAGE
# 想要字幕可加:response_format="srt"
)
return resp.text
except Exception as e:
import time
time.sleep(2**i * 0.5)
last=e
raise last

def hhmmss(sec: float)->str:
s=int(round(sec)); h=s//3600; m=(s%3600)//60; r=s%60
return f"{h:02d}:{m:02d}:{r:02d}"

def main():
api_key = os.environ.get("OPENAI_API_KEY")
if not api_key:
raise RuntimeError("請把 OPENAI_API_KEY 設在環境變數。")
client = OpenAI(api_key=api_key)

src = Path(INPUT_FILE)
if not src.exists():
raise FileNotFoundError(src)

meta = ffprobe_json(src)
total = get_duration(meta)
print(f"來源:{src.name} | 時長:{hhmmss(total)} | 大小:{src.stat().st_size/1024/1024:.2f} MB")

with tempfile.TemporaryDirectory() as td:
td = Path(td)
light = td / "light_master.mp3"
to_light_master(src, light)

boundaries = plan_boundaries(light) # 在輕量母檔上計算切點
print(f"將切成 {len(boundaries)} 段")

parts_dir = td / "parts"
parts = split_once(light, parts_dir, boundaries)

# 併發上傳
results = [""]*len(parts)
with ThreadPoolExecutor(max_workers=WORKERS) as ex:
futs = {ex.submit(transcribe_one, client, p): idx for idx,p in enumerate(parts)}
for fut in as_completed(futs):
idx = futs[fut]
text = fut.result()
# 簡單加上時間範圍頭
start = 0.0 if idx==0 else boundaries[idx-1]
end = boundaries[idx]
results[idx] = f"[{idx+1:02d}] {hhmmss(start)} → {hhmmss(end)}\n{text}\n"

final = "\n".join(results)
print("\n--- 合併結果 ---\n")
print(final)

if __name__ == "__main__":
main()

用 ffmpeg 統一音量

ffmpeg -i input.mp3 -af "loudnorm" normalized.mp3

解釋

整體目的

它是一個 自動音檔轉錄工具
會把輸入的音檔壓縮、切段,再送到 OpenAI 的轉錄 API(Whisper 模型)進行文字轉換,最後合併成完整的逐段文字稿。


處理流程

  1. 讀取設定
    指定要處理的檔案、使用的轉錄模型(例如 gpt-4o-mini-transcribe)、語言(例如中文 "zh")。
    設定限制:單段大小上限 (MAX_PART_MB)、單段時間上限 (MAX_SAFE_DURATION_SEC)、靜音判定門檻等。
  2. 讀取檔案資訊
    ffprobe 抓出音檔的長度、位元率、大小。
  3. 建立「輕量母檔」
    轉成單聲道、16kHz、低碼率 MP3,縮小檔案以加快後續切割與上傳。
  4. 決定切點
    若整檔很小且不長,直接送轉錄。
    否則:
    依檔案大小與位元率估算每段時間長度。
    在估計切點附近,嘗試對齊到「靜音點」,避免硬切在講話中間。
    確保每段不會過短(至少 10 秒)或過長(超過上限則再細分)。
  5. 實際切檔
    ffmpeg 把音檔依照計畫切成 part_001.mp3、part_002.mp3… 這樣的分段檔。
  6. 上傳並轉錄
    用 ThreadPoolExecutor 併發處理,多個分段同時送到 OpenAI API。
    每段最多重試五次,避免網路或 API 一時失敗。
  7. 組合輸出
    每段結果前面會標上段號與時間範圍,例如:
    [01] 00:00:0000:12:34
    這段的文字內容…
  8. 最後把所有段落合併印出。

關鍵設計點

  • 檔案大小與時長控制:避免單段太大導致 API 拒收。
  • 靜音對齊切割:讓切點更自然,不會把一句話切斷。
  • 輕量化轉檔:減少切檔與上傳所需的資源。
  • 多工併發:縮短整體轉錄時間。
  • 重試機制:提高穩定性。

使用方式

  1. 系統需安裝 ffmpegffprobe
  2. 設定環境變數 OPENAI_API_KEY
  3. INPUT_FILE 換成你的音檔路徑,直接執行程式。
  4. 螢幕上會輸出轉錄結果。



留言
avatar-img
留言分享你的想法!
avatar-img
Wei 的工程師聊什麼
3會員
6內容數
2025/09/18
本文探討AWS東京區域(ap-northeast-1)的Availability Zone apne1-az3即將於2025年2月28日終止服務的事件。文章分析事件背景、影響範圍以及可能原因,並參考相關文件與討論,提供完整的事件脈絡與技術說明。
2025/09/18
本文探討AWS東京區域(ap-northeast-1)的Availability Zone apne1-az3即將於2025年2月28日終止服務的事件。文章分析事件背景、影響範圍以及可能原因,並參考相關文件與討論,提供完整的事件脈絡與技術說明。
2025/09/14
情景 Windows Hyper-V 安裝 ubuntu server 24 時遇見錯誤。提示 The signed image's hash is not allowed (DB)。 解法 關閉 "啟用安全開機",並開啟 "啟用信賴平台模組"(如果你沒想根本解決,也可以選擇跳過此步驟)。
Thumbnail
2025/09/14
情景 Windows Hyper-V 安裝 ubuntu server 24 時遇見錯誤。提示 The signed image's hash is not allowed (DB)。 解法 關閉 "啟用安全開機",並開啟 "啟用信賴平台模組"(如果你沒想根本解決,也可以選擇跳過此步驟)。
Thumbnail
2024/03/04
用 Powershell 的 IDE ,寫一個在剪貼簿裡存 Timestamp 的無聊小程式。
Thumbnail
2024/03/04
用 Powershell 的 IDE ,寫一個在剪貼簿裡存 Timestamp 的無聊小程式。
Thumbnail
看更多
你可能也想看
Thumbnail
在小小的租屋房間裡,透過蝦皮購物平臺採購各種黏土、模型、美甲材料等創作素材,打造專屬黏土小宇宙的療癒過程。文中分享多個蝦皮挖寶地圖,並推薦蝦皮分潤計畫。
Thumbnail
在小小的租屋房間裡,透過蝦皮購物平臺採購各種黏土、模型、美甲材料等創作素材,打造專屬黏土小宇宙的療癒過程。文中分享多個蝦皮挖寶地圖,並推薦蝦皮分潤計畫。
Thumbnail
小蝸和小豬因購物習慣不同常起衝突,直到發現蝦皮分潤計畫,讓小豬的購物愛好產生價值,也讓小蝸開始欣賞另一半的興趣。想增加收入或改善伴侶間的購物觀念差異?讓蝦皮分潤計畫成為你們的神隊友吧!
Thumbnail
小蝸和小豬因購物習慣不同常起衝突,直到發現蝦皮分潤計畫,讓小豬的購物愛好產生價值,也讓小蝸開始欣賞另一半的興趣。想增加收入或改善伴侶間的購物觀念差異?讓蝦皮分潤計畫成為你們的神隊友吧!
Thumbnail
了解如何使用 Cloudflare Workers AI 與 Whisper 建立免費開源的語音辨識功能。本文詳細說明註冊步驟、部署流程及程式碼修改,讓你輕鬆將語音轉換成文字。
Thumbnail
了解如何使用 Cloudflare Workers AI 與 Whisper 建立免費開源的語音辨識功能。本文詳細說明註冊步驟、部署流程及程式碼修改,讓你輕鬆將語音轉換成文字。
Thumbnail
AI 生產力工具是一款免費、開源的應用程式,適用於 Windows 系統,整合了 ChatGPT 聊天和多個 AI 圖片/影片調整功能。提供完整、輕量兩種版本,差別在於輕量版沒有 ChatGPT 聊天。
Thumbnail
AI 生產力工具是一款免費、開源的應用程式,適用於 Windows 系統,整合了 ChatGPT 聊天和多個 AI 圖片/影片調整功能。提供完整、輕量兩種版本,差別在於輕量版沒有 ChatGPT 聊天。
Thumbnail
要做會議記錄或課程筆記,想做逐字稿卻苦於打字速度不夠快嗎?錄音再慢慢回放浪費時間又容易恍神?這篇文章包你滿意,不用再浪費時間爬文了,你需要的逐字稿神器在這裡,保母級教學!
Thumbnail
要做會議記錄或課程筆記,想做逐字稿卻苦於打字速度不夠快嗎?錄音再慢慢回放浪費時間又容易恍神?這篇文章包你滿意,不用再浪費時間爬文了,你需要的逐字稿神器在這裡,保母級教學!
Thumbnail
文章中,我們介紹了幾款免費的AI影片製作工具,並提供了使用教學和技巧。無論是Lumen5、Pictory、Canva、Kapwing、CapCut還是FlexClip,這些工具都能幫助你高效地實現影片製作目標。此外,我們還介紹了一些其他輔助工具,如AI配音工具和AI繪圖工具,讓您可以更豐富地製作影片
Thumbnail
文章中,我們介紹了幾款免費的AI影片製作工具,並提供了使用教學和技巧。無論是Lumen5、Pictory、Canva、Kapwing、CapCut還是FlexClip,這些工具都能幫助你高效地實現影片製作目標。此外,我們還介紹了一些其他輔助工具,如AI配音工具和AI繪圖工具,讓您可以更豐富地製作影片
Thumbnail
在數字化時代,PDF文件廣泛使用,但傳統處理方式顯得力不從心。本文推薦pdftopdf.ai等工具,通過OCR識別,將圖片中的文字轉化為可編輯、可搜索的文本。探討PDF文檔分析的AI工具,功能和價格。描述其用途以解決掃描件中文字無法直接搜索的困擾,提高工作效率。
Thumbnail
在數字化時代,PDF文件廣泛使用,但傳統處理方式顯得力不從心。本文推薦pdftopdf.ai等工具,通過OCR識別,將圖片中的文字轉化為可編輯、可搜索的文本。探討PDF文檔分析的AI工具,功能和價格。描述其用途以解決掃描件中文字無法直接搜索的困擾,提高工作效率。
Thumbnail
在當今數字化時代,需求日益增長。本文詳細介紹了幾種常用的PDF轉Word方法,並討論了它們的侷限性。接下來,我們將向您介紹pdftopdf.ai,一款具有先進的OCR和LLM技術,提供高效且保持原始文件格式和質量的解決方案。
Thumbnail
在當今數字化時代,需求日益增長。本文詳細介紹了幾種常用的PDF轉Word方法,並討論了它們的侷限性。接下來,我們將向您介紹pdftopdf.ai,一款具有先進的OCR和LLM技術,提供高效且保持原始文件格式和質量的解決方案。
Thumbnail
有許多影片編輯工具皆已導入 AI 技術,包括 AI 轉錄語音自動生成影片字幕。微軟旗下的 Clipchamp 線上影片編輯服務就有這項功能,登入 Microsoft 帳戶即可使用,支援轉錄各國語言,免費輸出 1080P 影片。
Thumbnail
有許多影片編輯工具皆已導入 AI 技術,包括 AI 轉錄語音自動生成影片字幕。微軟旗下的 Clipchamp 線上影片編輯服務就有這項功能,登入 Microsoft 帳戶即可使用,支援轉錄各國語言,免費輸出 1080P 影片。
Thumbnail
會不會有時候只是想要簡單的快速影片去背,剛好電腦沒有安裝適合的影片剪接軟體呢?pinokio這網站提共大量的AI小工具,只要安裝他在桌面,立即可獲最新的AI工具,並且他隨時更新最新版本,省下大量爬文搜尋時間,正適合不會程式的使用者一鍵無腦享受AI帶來的便利與快速。 這次介紹裡面一個好用的小工具"R
Thumbnail
會不會有時候只是想要簡單的快速影片去背,剛好電腦沒有安裝適合的影片剪接軟體呢?pinokio這網站提共大量的AI小工具,只要安裝他在桌面,立即可獲最新的AI工具,並且他隨時更新最新版本,省下大量爬文搜尋時間,正適合不會程式的使用者一鍵無腦享受AI帶來的便利與快速。 這次介紹裡面一個好用的小工具"R
Thumbnail
上一單元,我向你介紹了我使用的七項硬體工具。在這一單元,我接著要和你分享軟體層面,我正在使用的兩款軟體工具,我怎麼使用它們,以及我為什麼選擇它們。當硬體有了,軟體也要跟上,才能發揮好的生產力。
Thumbnail
上一單元,我向你介紹了我使用的七項硬體工具。在這一單元,我接著要和你分享軟體層面,我正在使用的兩款軟體工具,我怎麼使用它們,以及我為什麼選擇它們。當硬體有了,軟體也要跟上,才能發揮好的生產力。
Thumbnail
在數位時代,為了要應付各種場景和需求,所需要的檔案格式也不盡相同。撇除專用格式不談,日常使用的影音圖片格式,還要為個別種類去安裝對應的編輯軟體步驟多少會有些繁瑣。File Converter可以應付一些簡單的媒體格式轉換,過程中不用開啟任何軟體。可以省去不少步驟。
Thumbnail
在數位時代,為了要應付各種場景和需求,所需要的檔案格式也不盡相同。撇除專用格式不談,日常使用的影音圖片格式,還要為個別種類去安裝對應的編輯軟體步驟多少會有些繁瑣。File Converter可以應付一些簡單的媒體格式轉換,過程中不用開啟任何軟體。可以省去不少步驟。
追蹤感興趣的內容從 Google News 追蹤更多 vocus 的最新精選內容追蹤 Google News