MichaelChou0806 commited on
Commit
cf686cb
·
verified ·
1 Parent(s): d634404

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +27 -175
app.py CHANGED
@@ -4,7 +4,7 @@ import shutil
4
  from pydub import AudioSegment
5
  from openai import OpenAI
6
  import gradio as gr
7
- from fastapi import FastAPI, File, UploadFile, Form, HTTPException
8
 
9
  # ======================================================
10
  # 🔐 設定區
@@ -17,58 +17,6 @@ print("===== 🚀 啟動中 =====")
17
  print(f"APP_PASSWORD: {'✅ 已載入' if PASSWORD else '❌ 未載入'}")
18
  print(f"目前密碼內容:{PASSWORD}")
19
 
20
- # ======================================================
21
- # ⚔️ 防暴力破解
22
- # ======================================================
23
- MAX_FAILED_IN_WINDOW = 10
24
- WINDOW_SECONDS = 24 * 3600
25
- LOCK_DURATION_SECONDS = 24 * 3600
26
- SHORT_BURST_LIMIT = 5
27
- SHORT_BURST_SECONDS = 60
28
-
29
- attempts = {}
30
- locked = {}
31
-
32
- def _now():
33
- return int(time.time())
34
-
35
- def prune_old_attempts(sid):
36
- cutoff = _now() - WINDOW_SECONDS
37
- if sid in attempts:
38
- attempts[sid] = [t for t in attempts[sid] if t >= cutoff]
39
- if not attempts[sid]:
40
- del attempts[sid]
41
-
42
- def check_lock(sid):
43
- if sid in locked:
44
- if _now() < locked[sid]:
45
- remain = locked[sid] - _now()
46
- return True, f"🔒 已被鎖定,請 {remain // 60} 分鐘後再試。"
47
- else:
48
- locked.pop(sid, None)
49
- attempts.pop(sid, None)
50
- prune_old_attempts(sid)
51
- cnt = len(attempts.get(sid, []))
52
- if cnt >= MAX_FAILED_IN_WINDOW:
53
- locked[sid] = _now() + LOCK_DURATION_SECONDS
54
- return True, f"🔒 嘗試過多,已鎖定 24 小時。"
55
- return False, ""
56
-
57
- def record_failed_attempt(sid):
58
- now = _now()
59
- attempts.setdefault(sid, []).append(now)
60
- prune_old_attempts(sid)
61
- recent_cutoff = now - SHORT_BURST_SECONDS
62
- recent = [t for t in attempts[sid] if t >= recent_cutoff]
63
- if len(recent) >= SHORT_BURST_LIMIT:
64
- locked[sid] = now + 300
65
- return len(attempts[sid]), "⚠️ 多次快速嘗試,暫時鎖定5分鐘。"
66
- return len(attempts[sid]), ""
67
-
68
- def clear_attempts(sid):
69
- attempts.pop(sid, None)
70
- locked.pop(sid, None)
71
-
72
  # ======================================================
73
  # 🎧 音訊轉錄核心
74
  # ======================================================
@@ -88,8 +36,7 @@ def split_audio_if_needed(path):
88
  files.append(fn)
89
  return files
90
 
91
- def transcribe_core(path, model):
92
- # 1️⃣ 修正 LINE 語音 mp4 假副檔名
93
  if path and path.lower().endswith(".mp4"):
94
  fixed_path = path[:-4] + ".m4a"
95
  try:
@@ -97,9 +44,8 @@ def transcribe_core(path, model):
97
  path = fixed_path
98
  print("🔧 已自動修正 mp4 → m4a")
99
  except Exception as e:
100
- print(f"⚠️ mp4→m4a 複製失敗:{e},改用原檔嘗試")
101
 
102
- # 2️⃣ Whisper 轉錄
103
  chunks = split_audio_if_needed(path)
104
  raw_parts = []
105
  for f in chunks:
@@ -112,7 +58,7 @@ def transcribe_core(path, model):
112
  raw_parts.append(res)
113
  full_raw = "\n".join(raw_parts)
114
 
115
- # 3️⃣ 簡轉繁(不改寫內容)
116
  conv_prompt = (
117
  "請將以下內容完整轉換為「繁體中文(台灣用語)」:\n"
118
  "規則:1) 僅做簡→繁字形轉換;2) 不要意譯或改寫;3) 不要添加任何前後綴。\n"
@@ -128,11 +74,10 @@ def transcribe_core(path, model):
128
  )
129
  full_trad = trad_resp.choices[0].message.content.strip()
130
 
131
- # 4️⃣ 生成繁體摘要(自動決定條列與否)
132
  sum_prompt = (
133
- "請用台灣繁體中文撰寫摘要。"
134
- "若內容資訊較多,可條列出重點;若內容簡短,請用一句話概述即可。"
135
- "請勿添加前綴或評論,僅輸出摘要。\n\n" + full_trad
136
  )
137
  sum_resp = client.chat.completions.create(
138
  model="gpt-4o-mini",
@@ -143,126 +88,33 @@ def transcribe_core(path, model):
143
  temperature=0.2,
144
  )
145
  summ = sum_resp.choices[0].message.content.strip()
146
-
147
  return full_trad, summ
148
 
149
  # ======================================================
150
- # 🌐 FastAPI API(捷徑用)
151
  # ======================================================
152
- app = FastAPI(title="LINE Transcription API")
153
-
154
- @app.post("/api/transcribe")
155
- async def api_transcribe(
156
- file: UploadFile = File(...),
157
- token: str = Form(default=None)
158
- ):
159
- """供捷徑上傳音訊並取得 JSON"""
160
- if token != PASSWORD:
161
- raise HTTPException(status_code=403, detail="Invalid token")
162
-
163
- temp = file.filename
164
- with open(temp, "wb") as f:
165
- f.write(await file.read())
166
 
167
- text, summary = transcribe_core(temp, "whisper-1")
168
- os.remove(temp)
169
- return {"text": text, "summary": summary}
170
-
171
- @app.get("/health")
172
- def health():
173
- """捷徑可 ping 這個確認服務運作中"""
174
- return {"status": "ok", "time": int(time.time())}
175
-
176
- # ======================================================
177
- # 💬 Gradio 主介面
178
- # ======================================================
179
- def _normalize_upload_path(file_input):
180
- if not file_input:
181
- return None
182
- if isinstance(file_input, str):
183
- return file_input
184
- if isinstance(file_input, list) and file_input:
185
- return _normalize_upload_path(file_input[0])
186
- path = getattr(file_input, "name", None)
187
- if not path and isinstance(file_input, dict):
188
- path = file_input.get("name") or file_input.get("path")
189
- return path
190
-
191
- def transcribe_with_password(session_id, password, file_input, model_choice):
192
- password = password.strip().replace(" ", "").replace("\u200b", "")
193
- locked_flag, msg = check_lock(session_id)
194
- if locked_flag:
195
- return msg, "", ""
196
- if password != PASSWORD:
197
- cnt, msg2 = record_failed_attempt(session_id)
198
- return msg2 or f"密碼錯誤(第 {cnt} 次)", "", ""
199
- path = _normalize_upload_path(file_input)
200
- if not path or not os.path.exists(path):
201
- return "找不到上傳檔案,請重新選擇。", "", ""
202
- clear_attempts(session_id)
203
- full, summ = transcribe_core(path, model_choice)
204
- return "✅ 轉錄完成", full, summ
205
-
206
- def ask_about_transcript(full_text, q):
207
- if not full_text.strip():
208
- return "⚠️ 尚未有轉錄內容"
209
- if not q.strip():
210
- return "請輸入問題"
211
- prompt = f"以下是轉錄內容:\n{full_text}\n\n問題:{q}\n請用繁體中文回答。"
212
- res = client.chat.completions.create(
213
- model="gpt-4o-mini",
214
- messages=[{"role":"user","content":prompt}],
215
- temperature=0.6,
216
- )
217
- return res.choices[0].message.content.strip()
218
-
219
- # ======================================================
220
- # 🖥️ Gradio UI
221
- # ======================================================
222
  with gr.Blocks(theme=gr.themes.Soft()) as demo:
223
- gr.Markdown("## 🎧 語音轉錄與摘要工具(私人API勿轉傳|支援 iPhone LINE .mp4)")
224
- session_state = gr.State(value=None)
225
- with gr.Row():
226
- password_input = gr.Textbox(label="輸入密碼", placeholder="請輸入英文與數字(請切換成英文輸入法)", type="password", max_lines=1)
227
- model_choice = gr.Dropdown(["whisper-1", "gpt-4o-mini-transcribe"], value="whisper-1", label="選擇模型")
228
- file_input = gr.File(label="上傳音訊 / LINE 語音檔(支援 .m4a, .aac, .wav, .mp4)", file_count="single", file_types=["audio", ".mp4", ".m4a", ".aac", ".wav"])
229
- transcribe_btn = gr.Button("開始轉錄與摘要 🚀")
230
- status_box = gr.Textbox(label="狀態", interactive=False)
231
- transcript_box = gr.Textbox(label="完整轉錄文字", lines=10)
232
- copy_transcript = gr.Button("📋 複製轉錄文字")
233
- summary_box = gr.Textbox(label="摘要結果", lines=10)
234
- copy_summary = gr.Button("📋 複製摘要結果")
235
-
236
- with gr.Accordion("💬 進一步問 AI", open=False):
237
- user_q = gr.Textbox(label="輸入問題", lines=2)
238
- ask_btn = gr.Button("詢問 AI 🤔")
239
- ai_reply = gr.Textbox(label="AI 回覆", lines=6)
240
- copy_reply = gr.Button("📋 複製 AI 回覆")
241
-
242
- def init_session():
243
- import uuid
244
- return str(uuid.uuid4())
245
-
246
- demo.load(init_session, None, session_state)
247
- transcribe_btn.click(transcribe_with_password, [session_state, password_input, file_input, model_choice], [status_box, transcript_box, summary_box])
248
- ask_btn.click(ask_about_transcript, [transcript_box, user_q], [ai_reply])
249
-
250
- copy_js = """async (text) => {try {await navigator.clipboard.writeText(text); alert("✅ 已複製到剪貼簿!");} catch (e) {alert("❌ 複製失敗:" + e);}}"""
251
- copy_transcript.click(fn=None, inputs=transcript_box, outputs=None, js=copy_js)
252
- copy_summary.click(fn=None, inputs=summary_box, outputs=None, js=copy_js)
253
- copy_reply.click(fn=None, inputs=ai_reply, outputs=None, js=copy_js)
254
 
255
  # ======================================================
256
- # 🚀 啟動(Hugging Face 最終穩定版)
257
  # ======================================================
258
-
259
- # ✅ Hugging Face 會自動搜尋變數 `app` 作為入口
260
- # 所以我們要在這裡重新指派回 FastAPI + Gradio 結合後的物件
261
- app = gr.mount_gradio_app(app, demo, path="/")
262
-
263
- # ✅ 不要在 Hugging Face 上手動啟動 uvicorn
264
- # 若你要在本機測試,再用 python app.py 啟動即可
265
  if __name__ == "__main__":
266
- import uvicorn
267
- print("🌐 本地測試模式啟動中:http://127.0.0.1:7860")
268
- uvicorn.run(app, host="0.0.0.0", port=7860)
 
4
  from pydub import AudioSegment
5
  from openai import OpenAI
6
  import gradio as gr
7
+ from fastapi import HTTPException
8
 
9
  # ======================================================
10
  # 🔐 設定區
 
17
  print(f"APP_PASSWORD: {'✅ 已載入' if PASSWORD else '❌ 未載入'}")
18
  print(f"目前密碼內容:{PASSWORD}")
19
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  # ======================================================
21
  # 🎧 音訊轉錄核心
22
  # ======================================================
 
36
  files.append(fn)
37
  return files
38
 
39
+ def transcribe_core(path, model="whisper-1"):
 
40
  if path and path.lower().endswith(".mp4"):
41
  fixed_path = path[:-4] + ".m4a"
42
  try:
 
44
  path = fixed_path
45
  print("🔧 已自動修正 mp4 → m4a")
46
  except Exception as e:
47
+ print(f"⚠️ mp4→m4a 轉檔失敗:{e}")
48
 
 
49
  chunks = split_audio_if_needed(path)
50
  raw_parts = []
51
  for f in chunks:
 
58
  raw_parts.append(res)
59
  full_raw = "\n".join(raw_parts)
60
 
61
+ # 簡轉繁
62
  conv_prompt = (
63
  "請將以下內容完整轉換為「繁體中文(台灣用語)」:\n"
64
  "規則:1) 僅做簡→繁字形轉換;2) 不要意譯或改寫;3) 不要添加任何前後綴。\n"
 
74
  )
75
  full_trad = trad_resp.choices[0].message.content.strip()
76
 
77
+ # 摘要
78
  sum_prompt = (
79
+ "請用台灣繁體中文撰寫摘要。若內容資訊多,可條列出重點;若內容簡短,請用一句話概述即可。\n\n"
80
+ + full_trad
 
81
  )
82
  sum_resp = client.chat.completions.create(
83
  model="gpt-4o-mini",
 
88
  temperature=0.2,
89
  )
90
  summ = sum_resp.choices[0].message.content.strip()
 
91
  return full_trad, summ
92
 
93
  # ======================================================
94
+ # 💬 Gradio 介面
95
  # ======================================================
96
+ def transcribe_with_password(password, file):
97
+ if password.strip() != PASSWORD:
98
+ raise HTTPException(status_code=403, detail="密碼錯誤 ❌")
99
+ if not file:
100
+ return "⚠️ 未選擇��案", "", ""
101
+ text, summary = transcribe_core(file.name)
102
+ return "✅ 完成", text, summary
 
 
 
 
 
 
 
103
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  with gr.Blocks(theme=gr.themes.Soft()) as demo:
105
+ gr.Markdown("## 🎧 LINE 語音轉錄與摘要(支援 .m4a / .mp4)")
106
+ pw = gr.Textbox(label="輸入密碼", type="password")
107
+ f = gr.File(label="上傳音訊檔")
108
+ run = gr.Button("開始轉錄 🚀")
109
+ s = gr.Textbox(label="狀態", interactive=False)
110
+ t = gr.Textbox(label="轉錄結果", lines=10)
111
+ su = gr.Textbox(label="AI 摘要", lines=8)
112
+ run.click(transcribe_with_password, [pw, f], [s, t, su])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
 
114
  # ======================================================
115
+ # 🚀 啟動
116
  # ======================================================
 
 
 
 
 
 
 
117
  if __name__ == "__main__":
118
+ demo.launch(server_name="0.0.0.0", server_port=7860)
119
+ else:
120
+ demo.launch()