MichaelChou0806 commited on
Commit
21fed7f
·
verified ·
1 Parent(s): 0b13a9c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +169 -65
app.py CHANGED
@@ -6,77 +6,126 @@ from openai import OpenAI
6
  import gradio as gr
7
  from fastapi import FastAPI, File, UploadFile, Form, HTTPException
8
 
9
- # ========================
10
- # 🔐 基本設定
11
- # ========================
12
  PASSWORD = os.getenv("APP_PASSWORD", "defaultpass")
13
- OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
 
14
 
15
  print("===== 🚀 啟動中 =====")
16
- print(f"OPENAI_API_KEY: {'✅ 已載入' if OPENAI_API_KEY else '❌ 未載入'}")
17
  print(f"APP_PASSWORD: {'✅ 已載入' if PASSWORD else '❌ 未載入'}")
18
  print(f"目前密碼內容:{PASSWORD}")
19
 
20
- MAX_SIZE = 25 * 1024 * 1024
21
- client = OpenAI(api_key=OPENAI_API_KEY)
22
-
23
- # FastAPI App for捷徑 API
24
- app = FastAPI(title="LINE Audio Transcriber")
25
-
26
- # ========================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  # 🎧 音訊轉錄核心
28
- # ========================
29
- def split_audio_if_needed(path: str):
30
  size = os.path.getsize(path)
31
  if size <= MAX_SIZE:
32
  return [path]
33
  audio = AudioSegment.from_file(path)
34
- n = int(size / MAX_SIZE) + 1
35
- chunk_ms = len(audio) / n
36
- parts = []
37
- for i in range(n):
 
 
38
  fn = f"chunk_{i+1}.wav"
39
- audio[int(i * chunk_ms):int((i + 1) * chunk_ms)].export(fn, format="wav")
40
- parts.append(fn)
41
- return parts
42
-
43
 
44
- def transcribe_core(path: str, model: str = "whisper-1"):
45
- if path.lower().endswith(".mp4"):
46
- fixed = path[:-4] + ".m4a"
47
  try:
48
- shutil.copy(path, fixed)
49
- path = fixed
50
  print("🔧 已自動修正 mp4 → m4a")
51
  except Exception as e:
52
- print(f"⚠️ mp4→m4a 轉檔失敗:{e}")
53
 
54
  chunks = split_audio_if_needed(path)
55
  txts = []
56
  for f in chunks:
57
  with open(f, "rb") as af:
58
- t = client.audio.transcriptions.create(
59
  model=model, file=af, response_format="text"
60
  )
61
- txts.append(t)
62
  full = "\n".join(txts)
63
- summ = client.chat.completions.create(
64
  model="gpt-4o-mini",
65
- messages=[{"role": "user", "content": f"請用繁體中文摘要以下內容:\n{full}"}],
66
  temperature=0.4,
67
- ).choices[0].message.content.strip()
 
68
  return full, summ
69
 
 
 
 
 
70
 
71
- # ========================
72
- # 🌐 FastAPI 端點(捷徑用)
73
- # ========================
74
  @app.post("/api/transcribe")
75
  async def api_transcribe(
76
  file: UploadFile = File(...),
77
  token: str = Form(default=None)
78
  ):
79
- """ iPhone 捷徑上傳音訊並取得 JSON"""
80
  if token != PASSWORD:
81
  raise HTTPException(status_code=403, detail="Invalid token")
82
 
@@ -84,42 +133,97 @@ async def api_transcribe(
84
  with open(temp, "wb") as f:
85
  f.write(await file.read())
86
 
87
- text, summary = transcribe_core(temp)
88
  os.remove(temp)
89
  return {"text": text, "summary": summary}
90
 
91
-
92
  @app.get("/health")
93
  def health():
94
- """捷徑可先 ping 這個確認服務運作中"""
95
  return {"status": "ok", "time": int(time.time())}
96
 
97
-
98
- # ========================
99
- # 💬 Gradio 介面
100
- # ========================
101
- def transcribe_with_pw(password, file):
102
- if password.strip() != PASSWORD:
103
- return "❌ 密碼錯誤", "", ""
104
- if not file:
105
- return "⚠️ 未選擇檔案", "", ""
106
- text, summary = transcribe_core(file.name)
107
- return "✅ 完成", text, summary
108
-
109
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
  with gr.Blocks(theme=gr.themes.Soft()) as demo:
111
- gr.Markdown("## 🎧 LINE 語音轉錄與摘要工具(支援 .m4a / .mp4)")
112
- pw = gr.Textbox(label="輸入密碼", type="password")
113
- f = gr.File(label="上傳音訊檔 (.m4a/.mp3/.wav/.mp4)")
114
- run = gr.Button("開始轉錄 🚀")
115
- s = gr.Textbox(label="狀態", interactive=False)
116
- t = gr.Textbox(label="逐字稿", lines=10)
117
- su = gr.Textbox(label="摘要", lines=8)
118
- run.click(transcribe_with_pw, [pw, f], [s, t, su])
119
-
120
- # ========================
121
- # 🚀 啟動(單一埠)
122
- # ========================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
  gr.mount_gradio_app(app, demo, path="/")
124
 
125
  if __name__ == "__main__":
 
6
  import gradio as gr
7
  from fastapi import FastAPI, File, UploadFile, Form, HTTPException
8
 
9
+ # ======================================================
10
+ # 🔐 設定區
11
+ # ======================================================
12
  PASSWORD = os.getenv("APP_PASSWORD", "defaultpass")
13
+ MAX_SIZE = 25 * 1024 * 1024
14
+ client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
15
 
16
  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
+ # ======================================================
75
+ def split_audio_if_needed(path):
76
  size = os.path.getsize(path)
77
  if size <= MAX_SIZE:
78
  return [path]
79
  audio = AudioSegment.from_file(path)
80
+ num = int(size / MAX_SIZE) + 1
81
+ chunk_ms = len(audio) / num
82
+ files = []
83
+ for i in range(num):
84
+ start, end = int(i * chunk_ms), int((i + 1) * chunk_ms)
85
+ chunk = audio[start:end]
86
  fn = f"chunk_{i+1}.wav"
87
+ chunk.export(fn, format="wav")
88
+ files.append(fn)
89
+ return files
 
90
 
91
+ def transcribe_core(path, model):
92
+ if path and path.lower().endswith(".mp4"):
93
+ fixed_path = path[:-4] + ".m4a"
94
  try:
95
+ shutil.copy(path, fixed_path)
96
+ path = fixed_path
97
  print("🔧 已自動修正 mp4 → m4a")
98
  except Exception as e:
99
+ print(f"⚠️ mp4→m4a 複製失敗:{e},改用原檔嘗試")
100
 
101
  chunks = split_audio_if_needed(path)
102
  txts = []
103
  for f in chunks:
104
  with open(f, "rb") as af:
105
+ res = client.audio.transcriptions.create(
106
  model=model, file=af, response_format="text"
107
  )
108
+ txts.append(res)
109
  full = "\n".join(txts)
110
+ res = client.chat.completions.create(
111
  model="gpt-4o-mini",
112
+ messages=[{"role":"user","content":f"請用繁體中文摘要以下內容:\n{full}"}],
113
  temperature=0.4,
114
+ )
115
+ summ = res.choices[0].message.content.strip()
116
  return full, summ
117
 
118
+ # ======================================================
119
+ # 🌐 FastAPI API(iPhone 捷徑可用)
120
+ # ======================================================
121
+ app = FastAPI(title="LINE Transcription API")
122
 
 
 
 
123
  @app.post("/api/transcribe")
124
  async def api_transcribe(
125
  file: UploadFile = File(...),
126
  token: str = Form(default=None)
127
  ):
128
+ """供捷徑上傳音訊並取得 JSON"""
129
  if token != PASSWORD:
130
  raise HTTPException(status_code=403, detail="Invalid token")
131
 
 
133
  with open(temp, "wb") as f:
134
  f.write(await file.read())
135
 
136
+ text, summary = transcribe_core(temp, "whisper-1")
137
  os.remove(temp)
138
  return {"text": text, "summary": summary}
139
 
 
140
  @app.get("/health")
141
  def health():
142
+ """捷徑可 ping 這個確認服務運作中"""
143
  return {"status": "ok", "time": int(time.time())}
144
 
145
+ # ======================================================
146
+ # 💬 Gradio 介面(完整舊版)
147
+ # ======================================================
148
+ def _normalize_upload_path(file_input):
149
+ if not file_input:
150
+ return None
151
+ if isinstance(file_input, str):
152
+ return file_input
153
+ if isinstance(file_input, list) and file_input:
154
+ return _normalize_upload_path(file_input[0])
155
+ path = getattr(file_input, "name", None)
156
+ if not path and isinstance(file_input, dict):
157
+ path = file_input.get("name") or file_input.get("path")
158
+ return path
159
+
160
+ def transcribe_with_password(session_id, password, file_input, model_choice):
161
+ password = password.strip().replace(" ", "").replace("\u200b", "")
162
+ locked_flag, msg = check_lock(session_id)
163
+ if locked_flag:
164
+ return msg, "", ""
165
+ if password != PASSWORD:
166
+ cnt, msg2 = record_failed_attempt(session_id)
167
+ return msg2 or f"密碼錯誤(第 {cnt} 次)", "", ""
168
+ path = _normalize_upload_path(file_input)
169
+ if not path or not os.path.exists(path):
170
+ return "找不到上傳檔案,請重新選擇。", "", ""
171
+ clear_attempts(session_id)
172
+ full, summ = transcribe_core(path, model_choice)
173
+ return "✅ 轉錄完成", full, summ
174
+
175
+ def ask_about_transcript(full_text, q):
176
+ if not full_text.strip():
177
+ return "⚠️ 尚未有轉錄內容"
178
+ if not q.strip():
179
+ return "請輸入問題"
180
+ prompt = f"以下是轉錄內容:\n{full_text}\n\n問題:{q}\n請用繁體中文回答。"
181
+ res = client.chat.completions.create(
182
+ model="gpt-4o-mini",
183
+ messages=[{"role":"user","content":prompt}],
184
+ temperature=0.6,
185
+ )
186
+ return res.choices[0].message.content.strip()
187
+
188
+ # ======================================================
189
+ # 🖥️ Gradio UI
190
+ # ======================================================
191
  with gr.Blocks(theme=gr.themes.Soft()) as demo:
192
+ gr.Markdown("## 🎧 語音轉錄與摘要工具(私人API勿轉傳|支援 iPhone LINE .mp4)")
193
+ session_state = gr.State(value=None)
194
+ with gr.Row():
195
+ password_input = gr.Textbox(label="輸入密碼", placeholder="請輸入英文與數字(請切換成英文輸入法)", type="password", max_lines=1)
196
+ model_choice = gr.Dropdown(["whisper-1", "gpt-4o-mini-transcribe"], value="whisper-1", label="選擇模型")
197
+ file_input = gr.File(label="上傳音訊 / LINE 語音檔(支援 .m4a, .aac, .wav, .mp4)", file_count="single", file_types=["audio", ".mp4", ".m4a", ".aac", ".wav"])
198
+ transcribe_btn = gr.Button("開始轉錄與摘要 🚀")
199
+ status_box = gr.Textbox(label="狀態", interactive=False)
200
+ transcript_box = gr.Textbox(label="完整轉錄文字", lines=10)
201
+ copy_transcript = gr.Button("📋 複製轉錄文字")
202
+ summary_box = gr.Textbox(label="摘要結果", lines=10)
203
+ copy_summary = gr.Button("📋 複製摘要結果")
204
+
205
+ with gr.Accordion("💬 進一步問 AI", open=False):
206
+ user_q = gr.Textbox(label="輸入問題", lines=2)
207
+ ask_btn = gr.Button("詢問 AI 🤔")
208
+ ai_reply = gr.Textbox(label="AI 回覆", lines=6)
209
+ copy_reply = gr.Button("📋 複製 AI 回覆")
210
+
211
+ def init_session():
212
+ import uuid
213
+ return str(uuid.uuid4())
214
+
215
+ demo.load(init_session, None, session_state)
216
+ transcribe_btn.click(transcribe_with_password, [session_state, password_input, file_input, model_choice], [status_box, transcript_box, summary_box])
217
+ ask_btn.click(ask_about_transcript, [transcript_box, user_q], [ai_reply])
218
+
219
+ copy_js = """async (text) => {try {await navigator.clipboard.writeText(text); alert("✅ 已複製到剪貼簿!");} catch (e) {alert("❌ 複製失敗:" + e);}}"""
220
+ copy_transcript.click(fn=None, inputs=transcript_box, outputs=None, js=copy_js)
221
+ copy_summary.click(fn=None, inputs=summary_box, outputs=None, js=copy_js)
222
+ copy_reply.click(fn=None, inputs=ai_reply, outputs=None, js=copy_js)
223
+
224
+ # ======================================================
225
+ # 🚀 啟動(Hugging Face)
226
+ # ======================================================
227
  gr.mount_gradio_app(app, demo, path="/")
228
 
229
  if __name__ == "__main__":