| import os |
| import time |
| import shutil |
| from pydub import AudioSegment |
| from openai import OpenAI |
| import gradio as gr |
| from fastapi import HTTPException |
|
|
| |
| |
| |
| PASSWORD = os.getenv("APP_PASSWORD", "defaultpass") |
| MAX_SIZE = 25 * 1024 * 1024 |
| client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) |
|
|
| print("===== 🚀 啟動中 =====") |
| print(f"APP_PASSWORD: {'✅ 已載入' if PASSWORD else '❌ 未載入'}") |
| print(f"目前密碼內容:{PASSWORD}") |
|
|
| |
| |
| |
| def split_audio_if_needed(path): |
| size = os.path.getsize(path) |
| if size <= MAX_SIZE: |
| return [path] |
| audio = AudioSegment.from_file(path) |
| num = int(size / MAX_SIZE) + 1 |
| chunk_ms = len(audio) / num |
| files = [] |
| for i in range(num): |
| start, end = int(i * chunk_ms), int((i + 1) * chunk_ms) |
| chunk = audio[start:end] |
| fn = f"chunk_{i+1}.wav" |
| chunk.export(fn, format="wav") |
| files.append(fn) |
| return files |
|
|
| def transcribe_core(path, model="whisper-1"): |
| if path and path.lower().endswith(".mp4"): |
| fixed_path = path[:-4] + ".m4a" |
| try: |
| shutil.copy(path, fixed_path) |
| path = fixed_path |
| print("🔧 已自動修正 mp4 → m4a") |
| except Exception as e: |
| print(f"⚠️ mp4→m4a 轉檔失敗:{e}") |
|
|
| chunks = split_audio_if_needed(path) |
| raw_parts = [] |
| for f in chunks: |
| with open(f, "rb") as af: |
| res = client.audio.transcriptions.create( |
| model=model, |
| file=af, |
| response_format="text" |
| ) |
| raw_parts.append(res) |
| full_raw = "\n".join(raw_parts) |
|
|
| |
| conv_prompt = ( |
| "請將以下內容完整轉換為「繁體中文(台灣用語)」:\n" |
| "規則:1) 僅做簡→繁字形轉換;2) 不要意譯或改寫;3) 不要添加任何前後綴。\n" |
| "-----\n" + full_raw |
| ) |
| trad_resp = client.chat.completions.create( |
| model="gpt-4o-mini", |
| messages=[ |
| {"role": "system", "content": "你是嚴格的繁體中文轉換器,只進行字形轉換。"}, |
| {"role": "user", "content": conv_prompt} |
| ], |
| temperature=0.0, |
| ) |
| full_trad = trad_resp.choices[0].message.content.strip() |
|
|
| |
| sum_prompt = ( |
| "請用台灣繁體中文撰寫摘要。若內容資訊多,可條列出重點;若內容簡短,請用一句話概述即可。\n\n" |
| + full_trad |
| ) |
| sum_resp = client.chat.completions.create( |
| model="gpt-4o-mini", |
| messages=[ |
| {"role": "system", "content": "你是一位精準且嚴格使用台灣繁體中文的摘要助手。"}, |
| {"role": "user", "content": sum_prompt} |
| ], |
| temperature=0.2, |
| ) |
| summ = sum_resp.choices[0].message.content.strip() |
| return full_trad, summ |
|
|
| |
| |
| |
| def transcribe_with_password(password, file): |
| if password.strip() != PASSWORD: |
| raise HTTPException(status_code=403, detail="密碼錯誤 ❌") |
| if not file: |
| return "⚠️ 未選擇檔案", "", "" |
| text, summary = transcribe_core(file.name) |
| return "✅ 完成", text, summary |
|
|
| with gr.Blocks(theme=gr.themes.Soft()) as demo: |
| gr.Markdown("## 🎧 LINE 語音轉錄與摘要(支援 .m4a / .mp4)") |
| pw = gr.Textbox(label="輸入密碼", type="password") |
| f = gr.File(label="上傳音訊檔") |
| run = gr.Button("開始轉錄 🚀") |
| s = gr.Textbox(label="狀態", interactive=False) |
| t = gr.Textbox(label="轉錄結果", lines=10) |
| su = gr.Textbox(label="AI 摘要", lines=8) |
| run.click(transcribe_with_password, [pw, f], [s, t, su]) |
|
|
| |
| |
| |
| if __name__ == "__main__": |
| demo.launch(server_name="0.0.0.0", server_port=7860) |
| else: |
| demo.launch() |
|
|