| | import os |
| | import time |
| | import shutil |
| | from pydub import AudioSegment |
| | from openai import OpenAI |
| | import gradio as gr |
| | from fastapi import FastAPI, UploadFile, File, Form |
| | from threading import Thread |
| | import uvicorn |
| |
|
| | |
| | |
| | |
| | PASSWORD = os.getenv("APP_PASSWORD", "chou") |
| | 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) |
| | n = int(size / MAX_SIZE) + 1 |
| | chunk_ms = len(audio) / n |
| | parts = [] |
| | for i in range(n): |
| | fn = f"chunk_{i+1}.wav" |
| | audio[int(i * chunk_ms):int((i + 1) * chunk_ms)].export(fn, format="wav") |
| | parts.append(fn) |
| | return parts |
| |
|
| | def transcribe_core(path, model="whisper-1"): |
| | if path.lower().endswith(".mp4"): |
| | fixed = path[:-4] + ".m4a" |
| | try: |
| | shutil.copy(path, fixed) |
| | path = fixed |
| | except Exception as e: |
| | print(f"⚠️ mp4→m4a 轉檔失敗:{e}") |
| |
|
| | chunks = split_audio_if_needed(path) |
| | txts = [] |
| | for f in chunks: |
| | with open(f, "rb") as af: |
| | res = client.audio.transcriptions.create(model=model, file=af, response_format="text") |
| | txts.append(res) |
| | full_raw = "\n".join(txts) |
| |
|
| | conv_prompt = ( |
| | "請將以下內容完整轉換為「繁體中文(台灣用語)」:\n" |
| | "規則:1) 僅做簡→繁字形轉換;2) 不要意譯或改寫;3) 不要添加任何前後綴。\n-----\n" + full_raw |
| | ) |
| | trad = client.chat.completions.create( |
| | model="gpt-4o-mini", |
| | messages=[ |
| | {"role": "system", "content": "你是嚴格的繁體中文轉換器。"}, |
| | {"role": "user", "content": conv_prompt} |
| | ], |
| | temperature=0.0, |
| | ).choices[0].message.content.strip() |
| |
|
| | sum_prompt = ( |
| | "請用台灣繁體中文撰寫摘要。若內容資訊多,可條列出重點;" |
| | "若內容簡短,請用一句話概述即可。\n\n" + trad |
| | ) |
| | summ = client.chat.completions.create( |
| | model="gpt-4o-mini", |
| | messages=[ |
| | {"role": "system", "content": "你是一位精準且嚴格使用台灣繁體中文的摘要助手。"}, |
| | {"role": "user", "content": sum_prompt} |
| | ], |
| | temperature=0.2, |
| | ).choices[0].message.content.strip() |
| |
|
| | return trad, summ |
| |
|
| | |
| | |
| | |
| | api_app = FastAPI() |
| |
|
| | @api_app.post("/api/transcribe") |
| | async def api_transcribe(file: UploadFile = File(...), token: str = Form(...)): |
| | if token != PASSWORD: |
| | return {"error": "Invalid token"} |
| | temp = file.filename |
| | with open(temp, "wb") as f: |
| | f.write(await file.read()) |
| | text, summary = transcribe_core(temp) |
| | os.remove(temp) |
| | return {"text": text, "summary": summary} |
| |
|
| | |
| | |
| | |
| | def transcribe_with_password(password, file): |
| | if password.strip() != PASSWORD: |
| | return "❌ 密碼錯誤", "", "" |
| | 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]) |
| |
|
| | |
| | |
| | |
| | def run_api(): |
| | uvicorn.run(api_app, host="0.0.0.0", port=7861) |
| |
|
| | Thread(target=run_api, daemon=True).start() |
| | app = demo |
| |
|
| | if __name__ == "__main__": |
| | demo.launch(server_name="0.0.0.0", server_port=7860) |
| |
|