MichaelChou0806 commited on
Commit
bcfbee7
·
verified ·
1 Parent(s): 4946511

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +112 -30
app.py CHANGED
@@ -1,15 +1,77 @@
1
  import os
 
 
 
2
  from pydub import AudioSegment
3
  from openai import OpenAI
4
  import gradio as gr
5
 
6
  # ========================
7
- # 🔐 使用者設定區(安全)
8
  # ========================
9
  PASSWORD = os.getenv("APP_PASSWORD", "defaultpass")
10
- MAX_SIZE = 25 * 1024 * 1024
11
  client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  # ========================
14
  # 🔊 音訊分割
15
  # ========================
@@ -31,14 +93,9 @@ def split_audio_if_needed(input_path):
31
  return chunk_files
32
 
33
  # ========================
34
- # 🎧 轉錄 + 摘要
35
  # ========================
36
- def transcribe_and_summarize(password, file, model_choice):
37
- if password != PASSWORD:
38
- return "❌ 密碼錯誤,請重新輸入。", "", ""
39
- if not file:
40
- return "請上傳音訊檔。", "", ""
41
-
42
  chunks = split_audio_if_needed(file)
43
  transcripts = []
44
  for idx, f in enumerate(chunks, 1):
@@ -50,27 +107,42 @@ def transcribe_and_summarize(password, file, model_choice):
50
  )
51
  transcripts.append(text)
52
  full_text = "\n".join(transcripts)
53
-
54
  response = client.chat.completions.create(
55
  model="gpt-4o-mini",
56
  messages=[
57
  {"role": "system", "content": "你是一位精準且擅長摘要的助手。"},
58
- {"role": "user", "content": f"請用繁體中文摘要以下內容:\n{full_text}"}
59
  ],
60
  temperature=0.4,
61
  )
62
  summary = response.choices[0].message.content.strip()
63
- return full_text, summary, "✅ 已完成轉錄與摘要"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
 
65
  # ========================
66
  # 💬 進一步問 AI
67
  # ========================
68
  def ask_about_transcript(full_text, user_question):
69
  if not full_text.strip():
70
- return "⚠️ 尚未有轉錄內容,請先上傳音訊。"
71
  if not user_question.strip():
72
- return "請輸入想詢問的問題。"
73
- prompt = f"以下是語音轉錄的完整內容:\n\n{full_text}\n\n使用者的問題是:{user_question}\n\n請以繁體中文回答。"
74
  response = client.chat.completions.create(
75
  model="gpt-4o-mini",
76
  messages=[{"role": "user", "content": prompt}],
@@ -81,42 +153,53 @@ def ask_about_transcript(full_text, user_question):
81
  # ========================
82
  # 🌐 Gradio 介面
83
  # ========================
84
- with gr.Blocks(theme=gr.themes.Soft(), css=".gr-button {font-size: 16px;} .copy-btn {background: #e0e0e0;}") as demo:
85
- gr.Markdown("## 🎧 語音轉錄與摘要工具 (受密碼保護)")
86
- gr.Markdown("上傳音訊後,自動轉錄文字並生成摘要。可進一步詢問 AI 關於對話內容。")
 
 
 
 
 
 
 
 
 
 
 
 
 
87
 
88
  with gr.Row():
89
- password_input = gr.Textbox(label="輸入密碼", type="password", placeholder="請輸入授權密碼")
90
  model_choice = gr.Dropdown(
91
  choices=["whisper-1", "gpt-4o-mini-transcribe"],
92
  value="whisper-1",
93
  label="選擇轉錄模型"
94
  )
95
 
96
- audio_input = gr.Audio(type="filepath", label="上傳音訊檔 (.m4a, .aac, .wav)")
97
  transcribe_btn = gr.Button("開始轉錄與摘要 🚀")
98
 
99
  status_box = gr.Textbox(label="狀態", interactive=False)
100
-
101
  with gr.Row():
102
  transcript_box = gr.Textbox(label="完整轉錄文字", lines=10)
103
- copy_transcript = gr.Button("📋 複製", elem_classes="copy-btn")
104
 
105
  with gr.Row():
106
  summary_box = gr.Textbox(label="摘要結果", lines=10)
107
- copy_summary = gr.Button("📋 複製", elem_classes="copy-btn")
108
 
109
  with gr.Accordion("💬 進一步問 AI", open=False):
110
- user_question = gr.Textbox(label="輸入你的問題(例如:我該如何回應?)", lines=2)
111
  ask_btn = gr.Button("詢問 AI 🤔")
112
  ai_reply = gr.Textbox(label="AI 回覆", lines=6)
113
- copy_reply = gr.Button("📋 複製 AI 回覆", elem_classes="copy-btn")
114
 
115
- # 事件綁定
116
  transcribe_btn.click(
117
- fn=transcribe_and_summarize,
118
- inputs=[password_input, audio_input, model_choice],
119
- outputs=[transcript_box, summary_box, status_box]
120
  )
121
 
122
  ask_btn.click(
@@ -125,7 +208,6 @@ with gr.Blocks(theme=gr.themes.Soft(), css=".gr-button {font-size: 16px;} .copy-
125
  outputs=[ai_reply]
126
  )
127
 
128
- # 前端 JS 複製功能
129
  copy_js = """
130
  async (text) => {
131
  try {
 
1
  import os
2
+ import time
3
+ import uuid
4
+ from datetime import timedelta
5
  from pydub import AudioSegment
6
  from openai import OpenAI
7
  import gradio as gr
8
 
9
  # ========================
10
+ # 🔐 環境變數設定
11
  # ========================
12
  PASSWORD = os.getenv("APP_PASSWORD", "defaultpass")
13
+ MAX_SIZE = 25 * 1024 * 1024 # 25 MB 限制
14
  client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
15
 
16
+ # ========================
17
+ # ⚔️ 防暴力破解機制
18
+ # ========================
19
+ MAX_FAILED_IN_WINDOW = 10 # 24小時內最多10次
20
+ WINDOW_SECONDS = 24 * 3600
21
+ LOCK_DURATION_SECONDS = 24 * 3600 # 鎖24小時
22
+ SHORT_BURST_LIMIT = 5 # 一分鐘內最多5次
23
+ SHORT_BURST_SECONDS = 60
24
+ attempts = {} # {session_id: [timestamps]}
25
+ locked = {} # {session_id: unlock_time}
26
+
27
+
28
+ def _now():
29
+ return int(time.time())
30
+
31
+
32
+ def prune_old_attempts(session_id):
33
+ if session_id not in attempts:
34
+ return
35
+ cutoff = _now() - WINDOW_SECONDS
36
+ attempts[session_id] = [t for t in attempts[session_id] if t >= cutoff]
37
+ if not attempts[session_id]:
38
+ del attempts[session_id]
39
+
40
+
41
+ def check_lock(session_id):
42
+ if session_id in locked:
43
+ unlock = locked[session_id]
44
+ if _now() < unlock:
45
+ remain = unlock - _now()
46
+ return True, f"🔒 已被鎖定,請 {remain // 60} 分鐘後再試。"
47
+ else:
48
+ del locked[session_id]
49
+ attempts.pop(session_id, None)
50
+ prune_old_attempts(session_id)
51
+ cnt = len(attempts.get(session_id, []))
52
+ if cnt >= MAX_FAILED_IN_WINDOW:
53
+ locked[session_id] = _now() + LOCK_DURATION_SECONDS
54
+ return True, f"🔒 嘗試過多,已鎖定 24 小時。"
55
+ return False, ""
56
+
57
+
58
+ def record_failed_attempt(session_id):
59
+ now = _now()
60
+ attempts.setdefault(session_id, []).append(now)
61
+ prune_old_attempts(session_id)
62
+ cnt = len(attempts.get(session_id, []))
63
+ recent_cutoff = now - SHORT_BURST_SECONDS
64
+ recent = [t for t in attempts[session_id] if t >= recent_cutoff]
65
+ if len(recent) >= SHORT_BURST_LIMIT:
66
+ locked[session_id] = now + 300 # 鎖5分鐘
67
+ return cnt, "⚠️ 多次快速嘗試,暫時鎖定5分鐘。"
68
+ return cnt, ""
69
+
70
+
71
+ def clear_attempts(session_id):
72
+ attempts.pop(session_id, None)
73
+ locked.pop(session_id, None)
74
+
75
  # ========================
76
  # 🔊 音訊分割
77
  # ========================
 
93
  return chunk_files
94
 
95
  # ========================
96
+ # 🎧 轉錄與摘要
97
  # ========================
98
+ def transcribe_core(file, model_choice):
 
 
 
 
 
99
  chunks = split_audio_if_needed(file)
100
  transcripts = []
101
  for idx, f in enumerate(chunks, 1):
 
107
  )
108
  transcripts.append(text)
109
  full_text = "\n".join(transcripts)
 
110
  response = client.chat.completions.create(
111
  model="gpt-4o-mini",
112
  messages=[
113
  {"role": "system", "content": "你是一位精準且擅長摘要的助手。"},
114
+ {"role": "user", "content": "請用繁體中文摘要以下內容:\n" + full_text}
115
  ],
116
  temperature=0.4,
117
  )
118
  summary = response.choices[0].message.content.strip()
119
+ return full_text, summary
120
+
121
+
122
+ def transcribe_with_password(session_id, password, file, model_choice):
123
+ locked_flag, msg = check_lock(session_id)
124
+ if locked_flag:
125
+ return msg, "", "", ""
126
+ if password != PASSWORD:
127
+ cnt, msg2 = record_failed_attempt(session_id)
128
+ if msg2:
129
+ return msg2, "", "", ""
130
+ return f"密碼錯誤(第 {cnt} 次)", "", "", ""
131
+ if not file:
132
+ return "請上傳音訊檔。", "", "", ""
133
+ clear_attempts(session_id)
134
+ full_text, summary = transcribe_core(file, model_choice)
135
+ return "✅ 成功轉錄與摘要完成", full_text, summary, ""
136
 
137
  # ========================
138
  # 💬 進一步問 AI
139
  # ========================
140
  def ask_about_transcript(full_text, user_question):
141
  if not full_text.strip():
142
+ return "⚠️ 尚未有轉錄內容。"
143
  if not user_question.strip():
144
+ return "請輸入問題。"
145
+ prompt = f"以下是轉錄內容:\n{full_text}\n\n使用者問:{user_question}\n請用繁體中文回答。"
146
  response = client.chat.completions.create(
147
  model="gpt-4o-mini",
148
  messages=[{"role": "user", "content": prompt}],
 
153
  # ========================
154
  # 🌐 Gradio 介面
155
  # ========================
156
+ with gr.Blocks(theme=gr.themes.Soft()) as demo:
157
+ gr.Markdown("## 🎧 語音轉錄與摘要工具(含防暴力破解)")
158
+
159
+ session_id_box = gr.Textbox(value="", visible=False)
160
+ init_session_js = """
161
+ () => {
162
+ let sid = localStorage.getItem('my_session_id');
163
+ if (!sid) {
164
+ sid = crypto.randomUUID ? crypto.randomUUID() :
165
+ (Date.now().toString(36) + Math.random().toString(36).slice(2));
166
+ localStorage.setItem('my_session_id', sid);
167
+ }
168
+ return sid;
169
+ }
170
+ """
171
+ session_id_box.load(_js=init_session_js)
172
 
173
  with gr.Row():
174
+ password_input = gr.Textbox(label="輸入密碼", type="password")
175
  model_choice = gr.Dropdown(
176
  choices=["whisper-1", "gpt-4o-mini-transcribe"],
177
  value="whisper-1",
178
  label="選擇轉錄模型"
179
  )
180
 
181
+ audio_input = gr.Audio(type="filepath", label="上傳音訊 (.m4a, .aac, .wav)")
182
  transcribe_btn = gr.Button("開始轉錄與摘要 🚀")
183
 
184
  status_box = gr.Textbox(label="狀態", interactive=False)
 
185
  with gr.Row():
186
  transcript_box = gr.Textbox(label="完整轉錄文字", lines=10)
187
+ copy_transcript = gr.Button("📋 複製")
188
 
189
  with gr.Row():
190
  summary_box = gr.Textbox(label="摘要結果", lines=10)
191
+ copy_summary = gr.Button("📋 複製")
192
 
193
  with gr.Accordion("💬 進一步問 AI", open=False):
194
+ user_question = gr.Textbox(label="輸入你的問題", lines=2)
195
  ask_btn = gr.Button("詢問 AI 🤔")
196
  ai_reply = gr.Textbox(label="AI 回覆", lines=6)
197
+ copy_reply = gr.Button("📋 複製")
198
 
 
199
  transcribe_btn.click(
200
+ fn=transcribe_with_password,
201
+ inputs=[session_id_box, password_input, audio_input, model_choice],
202
+ outputs=[status_box, transcript_box, summary_box, gr.Textbox(visible=False)]
203
  )
204
 
205
  ask_btn.click(
 
208
  outputs=[ai_reply]
209
  )
210
 
 
211
  copy_js = """
212
  async (text) => {
213
  try {