Update app.py
Browse files
app.py
CHANGED
|
@@ -83,18 +83,25 @@ def split_audio_if_needed(path):
|
|
| 83 |
return files
|
| 84 |
|
| 85 |
def transcribe_core(path, model):
|
| 86 |
-
# ✅
|
| 87 |
-
if path.lower().endswith(".mp4"):
|
| 88 |
fixed_path = path[:-4] + ".m4a"
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
|
|
|
|
|
|
|
|
|
| 92 |
|
| 93 |
chunks = split_audio_if_needed(path)
|
| 94 |
txts = []
|
| 95 |
for f in chunks:
|
| 96 |
with open(f, "rb") as af:
|
| 97 |
-
res = client.audio.transcriptions.create(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
txts.append(res)
|
| 99 |
full = "\n".join(txts)
|
| 100 |
res = client.chat.completions.create(
|
|
@@ -108,18 +115,38 @@ def transcribe_core(path, model):
|
|
| 108 |
# ========================
|
| 109 |
# 💬 主流程
|
| 110 |
# ========================
|
| 111 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
password = password.strip().replace(" ", "").replace("\u200b", "")
|
|
|
|
| 113 |
locked_flag, msg = check_lock(session_id)
|
| 114 |
if locked_flag:
|
| 115 |
return msg, "", ""
|
|
|
|
| 116 |
if password != PASSWORD:
|
| 117 |
cnt, msg2 = record_failed_attempt(session_id)
|
| 118 |
return msg2 or f"密碼錯誤(第 {cnt} 次)", "", ""
|
| 119 |
-
|
| 120 |
-
|
|
|
|
|
|
|
|
|
|
| 121 |
clear_attempts(session_id)
|
| 122 |
-
full, summ = transcribe_core(
|
| 123 |
return "✅ 轉錄完成", full, summ
|
| 124 |
|
| 125 |
def ask_about_transcript(full_text, q):
|
|
@@ -129,14 +156,17 @@ def ask_about_transcript(full_text, q):
|
|
| 129 |
return "請輸入問題"
|
| 130 |
prompt = f"以下是轉錄內容:\n{full_text}\n\n問題:{q}\n請用繁體中文回答。"
|
| 131 |
res = client.chat.completions.create(
|
| 132 |
-
model="gpt-4o-mini",
|
|
|
|
|
|
|
|
|
|
| 133 |
return res.choices[0].message.content.strip()
|
| 134 |
|
| 135 |
# ========================
|
| 136 |
# 🌐 Gradio 介面
|
| 137 |
# ========================
|
| 138 |
with gr.Blocks(theme=gr.themes.Soft()) as demo:
|
| 139 |
-
gr.Markdown("## 🎧 語音轉錄與摘要工具(私人API勿轉傳)")
|
| 140 |
|
| 141 |
session_state = gr.State(value=None)
|
| 142 |
|
|
@@ -153,7 +183,13 @@ with gr.Blocks(theme=gr.themes.Soft()) as demo:
|
|
| 153 |
label="選擇模型"
|
| 154 |
)
|
| 155 |
|
| 156 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
transcribe_btn = gr.Button("開始轉錄與摘要 🚀")
|
| 158 |
status_box = gr.Textbox(label="狀態", interactive=False)
|
| 159 |
transcript_box = gr.Textbox(label="完整轉錄文字", lines=10)
|
|
@@ -174,12 +210,12 @@ with gr.Blocks(theme=gr.themes.Soft()) as demo:
|
|
| 174 |
|
| 175 |
transcribe_btn.click(
|
| 176 |
transcribe_with_password,
|
| 177 |
-
[session_state, password_input,
|
| 178 |
[status_box, transcript_box, summary_box],
|
| 179 |
)
|
| 180 |
ask_btn.click(ask_about_transcript, [transcript_box, user_q], [ai_reply])
|
| 181 |
|
| 182 |
-
# ✅
|
| 183 |
copy_js = """
|
| 184 |
async (text) => {
|
| 185 |
try {
|
|
|
|
| 83 |
return files
|
| 84 |
|
| 85 |
def transcribe_core(path, model):
|
| 86 |
+
# ✅ iPhone LINE 語音(mp4 audio-only)— 不轉檔,只複製改副檔名
|
| 87 |
+
if path and path.lower().endswith(".mp4"):
|
| 88 |
fixed_path = path[:-4] + ".m4a"
|
| 89 |
+
try:
|
| 90 |
+
shutil.copy(path, fixed_path)
|
| 91 |
+
path = fixed_path
|
| 92 |
+
print("🔧 已自動修正 mp4 → m4a")
|
| 93 |
+
except Exception as e:
|
| 94 |
+
print(f"⚠️ mp4→m4a 複製失敗:{e},改用原檔嘗試")
|
| 95 |
|
| 96 |
chunks = split_audio_if_needed(path)
|
| 97 |
txts = []
|
| 98 |
for f in chunks:
|
| 99 |
with open(f, "rb") as af:
|
| 100 |
+
res = client.audio.transcriptions.create(
|
| 101 |
+
model=model,
|
| 102 |
+
file=af,
|
| 103 |
+
response_format="text"
|
| 104 |
+
)
|
| 105 |
txts.append(res)
|
| 106 |
full = "\n".join(txts)
|
| 107 |
res = client.chat.completions.create(
|
|
|
|
| 115 |
# ========================
|
| 116 |
# 💬 主流程
|
| 117 |
# ========================
|
| 118 |
+
def _normalize_upload_path(file_input):
|
| 119 |
+
"""兼容 gr.File 回傳型態:字串 / 物件 / dict / list"""
|
| 120 |
+
if not file_input:
|
| 121 |
+
return None
|
| 122 |
+
if isinstance(file_input, str):
|
| 123 |
+
return file_input
|
| 124 |
+
if isinstance(file_input, list) and file_input:
|
| 125 |
+
return _normalize_upload_path(file_input[0])
|
| 126 |
+
# gradio 有時傳 UploadedFile 物件或 dict
|
| 127 |
+
path = getattr(file_input, "name", None)
|
| 128 |
+
if not path and isinstance(file_input, dict):
|
| 129 |
+
path = file_input.get("name") or file_input.get("path")
|
| 130 |
+
return path
|
| 131 |
+
|
| 132 |
+
def transcribe_with_password(session_id, password, file_input, model_choice):
|
| 133 |
+
# 修正注音輸入造成的隱藏字元
|
| 134 |
password = password.strip().replace(" ", "").replace("\u200b", "")
|
| 135 |
+
|
| 136 |
locked_flag, msg = check_lock(session_id)
|
| 137 |
if locked_flag:
|
| 138 |
return msg, "", ""
|
| 139 |
+
|
| 140 |
if password != PASSWORD:
|
| 141 |
cnt, msg2 = record_failed_attempt(session_id)
|
| 142 |
return msg2 or f"密碼錯誤(第 {cnt} 次)", "", ""
|
| 143 |
+
|
| 144 |
+
path = _normalize_upload_path(file_input)
|
| 145 |
+
if not path or not os.path.exists(path):
|
| 146 |
+
return "找不到上傳檔案,請重新選擇。", "", ""
|
| 147 |
+
|
| 148 |
clear_attempts(session_id)
|
| 149 |
+
full, summ = transcribe_core(path, model_choice)
|
| 150 |
return "✅ 轉錄完成", full, summ
|
| 151 |
|
| 152 |
def ask_about_transcript(full_text, q):
|
|
|
|
| 156 |
return "請輸入問題"
|
| 157 |
prompt = f"以下是轉錄內容:\n{full_text}\n\n問題:{q}\n請用繁體中文回答。"
|
| 158 |
res = client.chat.completions.create(
|
| 159 |
+
model="gpt-4o-mini",
|
| 160 |
+
messages=[{"role":"user","content":prompt}],
|
| 161 |
+
temperature=0.6,
|
| 162 |
+
)
|
| 163 |
return res.choices[0].message.content.strip()
|
| 164 |
|
| 165 |
# ========================
|
| 166 |
# 🌐 Gradio 介面
|
| 167 |
# ========================
|
| 168 |
with gr.Blocks(theme=gr.themes.Soft()) as demo:
|
| 169 |
+
gr.Markdown("## 🎧 語音轉錄與摘要工具(私人API勿轉傳|支援 iPhone LINE .mp4)")
|
| 170 |
|
| 171 |
session_state = gr.State(value=None)
|
| 172 |
|
|
|
|
| 183 |
label="選擇模型"
|
| 184 |
)
|
| 185 |
|
| 186 |
+
# ✅ 用 File 而不是 Audio,允許 video/mp4 以及常見音訊附檔名
|
| 187 |
+
file_input = gr.File(
|
| 188 |
+
label="上傳音訊 / LINE 語音檔(支援 .m4a, .aac, .wav, .mp4)",
|
| 189 |
+
file_count="single",
|
| 190 |
+
file_types=["audio", ".mp4", ".m4a", ".aac", ".wav"]
|
| 191 |
+
)
|
| 192 |
+
|
| 193 |
transcribe_btn = gr.Button("開始轉錄與摘要 🚀")
|
| 194 |
status_box = gr.Textbox(label="狀態", interactive=False)
|
| 195 |
transcript_box = gr.Textbox(label="完整轉錄文字", lines=10)
|
|
|
|
| 210 |
|
| 211 |
transcribe_btn.click(
|
| 212 |
transcribe_with_password,
|
| 213 |
+
[session_state, password_input, file_input, model_choice],
|
| 214 |
[status_box, transcript_box, summary_box],
|
| 215 |
)
|
| 216 |
ask_btn.click(ask_about_transcript, [transcript_box, user_q], [ai_reply])
|
| 217 |
|
| 218 |
+
# ✅ 複製(Gradio 5.x)
|
| 219 |
copy_js = """
|
| 220 |
async (text) => {
|
| 221 |
try {
|