Ander1 commited on
Commit
4903e5a
·
verified ·
1 Parent(s): 62b60c0

Upload 11 files

Browse files
Files changed (12) hide show
  1. .gitattributes +1 -0
  2. DOCS.md +47 -0
  3. README.md +35 -7
  4. app.py +172 -0
  5. elevenlabs_stt.py +119 -0
  6. main_app.py +384 -0
  7. packages.txt +2 -0
  8. requirements.txt +17 -0
  9. temp_podcast_testo_TRAVERSE.mp3 +3 -0
  10. transcript_refiner.py +144 -0
  11. utils.py +94 -0
  12. whisper_stt.py +84 -0
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ temp_podcast_testo_TRAVERSE.mp3 filter=lfs diff=lfs merge=lfs -text
DOCS.md ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 音訊轉文字與優化系統使用說明
2
+
3
+ ## 功能介紹
4
+
5
+ 這個應用程式提供以下功能:
6
+
7
+ 1. 音訊轉文字(支援 Whisper 和 ElevenLabs)
8
+ 2. 文字優化和摘要生成
9
+ 3. 多語言支援
10
+ 4. Token 使用量和費用計算
11
+
12
+ ## 使用步驟
13
+
14
+ 1. **上傳音訊檔案**
15
+ - 支援格式:MP3、WAV、OGG、M4A
16
+ - 檔案大小限制:25MB
17
+
18
+ 2. **輸入 API 金鑰**
19
+ - OpenAI API 金鑰(必須)
20
+ - ElevenLabs API 金鑰(使用 ElevenLabs 服務時必須)
21
+
22
+ 3. **選擇服務和設定**
23
+ - 轉錄服務:Whisper 或 ElevenLabs
24
+ - OpenAI 模型:選擇用於文字優化的模型
25
+ - 語言:指定音訊的語言(可選)
26
+ - 說話者辨識:僅適用於 ElevenLabs
27
+ - 創意程度:調整文字優化的創意程度
28
+
29
+ 4. **處理和結果**
30
+ - 點擊「處理音訊」按鈕
31
+ - 查看原始轉錄文字
32
+ - 查看優化後文字
33
+ - 檢視 Token 使用量
34
+ - 檢視費用資訊
35
+
36
+ ## 安全性說明
37
+
38
+ - API 金鑰僅在當前處理中使用
39
+ - 不會儲存任何敏感資訊
40
+ - 每次使用需重新輸入 API 金鑰
41
+
42
+ ## 注意事項
43
+
44
+ 1. 確保網路連線穩定
45
+ 2. 使用高品質音訊以獲得更好的轉錄效果
46
+ 3. 注意 API 使用額度
47
+ 4. 建議使用支援的音訊格式
README.md CHANGED
@@ -1,13 +1,41 @@
1
  ---
2
- title: Audio2text
3
- emoji: 📚
4
- colorFrom: red
5
- colorTo: blue
6
  sdk: gradio
7
- sdk_version: 5.22.0
8
  app_file: app.py
9
  pinned: false
10
- short_description: audio transcribe to text and summary
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: 音訊轉文字與優化系統
3
+ emoji: 🎙️
4
+ colorFrom: blue
5
+ colorTo: purple
6
  sdk: gradio
7
+ sdk_version: 4.19.2
8
  app_file: app.py
9
  pinned: false
 
10
  ---
11
 
12
+ # 音訊轉文字與優化系統
13
+
14
+ 這是一個使用 Gradio 建立的音訊轉文字應用程式,支援多種功能:
15
+
16
+ ## 主要功能
17
+
18
+ - 音訊轉文字(支援 Whisper 和 ElevenLabs)
19
+ - 文字優化和摘要生成
20
+ - 多語言支援
21
+ - Token 使用量和費用計算
22
+
23
+ ## 使用方法
24
+
25
+ 1. 上傳音訊檔案
26
+ 2. 輸入必要的 API 金鑰
27
+ 3. 選擇轉錄服務和模型
28
+ 4. 設定語言選項
29
+ 5. 點擊處理按鈕
30
+
31
+ ## 安全性說明
32
+
33
+ - API 金鑰僅在當前處理中使用
34
+ - 不會儲存任何敏感資訊
35
+ - 每次使用需重新輸入 API 金鑰
36
+
37
+ ## 作者
38
+
39
+ **Tseng Yao Hsien**
40
+ Endocrinologist
41
+ Tungs' Taichung MetroHarbor Hospital
app.py ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import os
3
+ from elevenlabs_stt import transcribe_audio_elevenlabs
4
+ from whisper_stt import transcribe_audio_whisper
5
+ from transcript_refiner import refine_transcript
6
+ from utils import calculate_tokens_and_cost, OPENAI_MODELS, MODEL_PRICES
7
+
8
+ def process_audio(
9
+ audio_file,
10
+ openai_api_key,
11
+ elevenlabs_api_key,
12
+ service_choice,
13
+ openai_model,
14
+ language,
15
+ speaker_detection=False,
16
+ creativity=0.5
17
+ ):
18
+ try:
19
+ if not openai_api_key or len(openai_api_key) < 20:
20
+ return "請輸入有效的 OpenAI API 金鑰", "", "", ""
21
+
22
+ if service_choice == "ElevenLabs" and (not elevenlabs_api_key or len(elevenlabs_api_key) < 20):
23
+ return "請輸入有效的 ElevenLabs API 金鑰", "", "", ""
24
+
25
+ # 音訊轉文字
26
+ if service_choice == "ElevenLabs":
27
+ transcript = transcribe_audio_elevenlabs(
28
+ audio_file,
29
+ elevenlabs_api_key,
30
+ language=language,
31
+ speaker_detection=speaker_detection
32
+ )
33
+ else: # Whisper
34
+ transcript = transcribe_audio_whisper(
35
+ audio_file,
36
+ language=language
37
+ )
38
+
39
+ # 優化文字
40
+ refined_text = refine_transcript(
41
+ transcript,
42
+ openai_api_key,
43
+ openai_model,
44
+ creativity
45
+ )
46
+
47
+ # 計算 token 和費用
48
+ tokens_info, cost_info = calculate_tokens_and_cost(
49
+ transcript,
50
+ refined_text,
51
+ openai_model
52
+ )
53
+
54
+ return transcript, refined_text, tokens_info, cost_info
55
+
56
+ except Exception as e:
57
+ return f"錯誤:{str(e)}", "", "", ""
58
+
59
+ finally:
60
+ # 清除敏感資訊
61
+ if 'openai_api_key' in locals():
62
+ del openai_api_key
63
+ if 'elevenlabs_api_key' in locals():
64
+ del elevenlabs_api_key
65
+
66
+ # 創建 Gradio 介面
67
+ with gr.Blocks() as demo:
68
+ gr.Markdown("# 音訊轉文字與優化系統")
69
+
70
+ with gr.Row():
71
+ with gr.Column():
72
+ audio_input = gr.Audio(
73
+ label="上傳音訊檔案",
74
+ type="filepath"
75
+ )
76
+
77
+ with gr.Row():
78
+ openai_key = gr.Textbox(
79
+ label="OpenAI API 金鑰",
80
+ placeholder="輸入您的 OpenAI API 金鑰",
81
+ type="password",
82
+ value="",
83
+ every=None
84
+ )
85
+ elevenlabs_key = gr.Textbox(
86
+ label="ElevenLabs API 金鑰",
87
+ placeholder="輸入您的 ElevenLabs API 金鑰(如果使用 ElevenLabs)",
88
+ type="password",
89
+ value="",
90
+ every=None
91
+ )
92
+
93
+ service = gr.Radio(
94
+ choices=["Whisper", "ElevenLabs"],
95
+ label="選擇轉錄服務",
96
+ value="Whisper"
97
+ )
98
+
99
+ model = gr.Dropdown(
100
+ choices=list(OPENAI_MODELS.keys()),
101
+ label="選擇 OpenAI 模型",
102
+ value="gpt-3.5-turbo"
103
+ )
104
+
105
+ language = gr.Textbox(
106
+ label="語言(可選)",
107
+ placeholder="輸入語言代碼,例如:zh-TW、en、ja",
108
+ value=""
109
+ )
110
+
111
+ speaker = gr.Checkbox(
112
+ label="啟用說話者辨識(僅限 ElevenLabs)",
113
+ value=False
114
+ )
115
+
116
+ creativity = gr.Slider(
117
+ minimum=0,
118
+ maximum=1,
119
+ value=0.5,
120
+ label="創意程度"
121
+ )
122
+
123
+ process_btn = gr.Button("處理音訊")
124
+
125
+ with gr.Column():
126
+ original_output = gr.Textbox(
127
+ label="原始轉錄文字",
128
+ lines=10
129
+ )
130
+ refined_output = gr.Textbox(
131
+ label="優化後文字",
132
+ lines=10
133
+ )
134
+ token_info = gr.Textbox(
135
+ label="Token 使用資訊",
136
+ lines=3
137
+ )
138
+ cost_info = gr.Textbox(
139
+ label="費用資訊",
140
+ lines=3
141
+ )
142
+
143
+ gr.Markdown("""
144
+ ### 安全性說明
145
+ - API 金鑰僅在當前處理中使用
146
+ - 不會儲存任何敏感資訊
147
+ - 每次使用需重新輸入 API 金鑰
148
+ """)
149
+
150
+ # 設定處理函數
151
+ process_btn.click(
152
+ fn=process_audio,
153
+ inputs=[
154
+ audio_input,
155
+ openai_key,
156
+ elevenlabs_key,
157
+ service,
158
+ model,
159
+ language,
160
+ speaker,
161
+ creativity
162
+ ],
163
+ outputs=[
164
+ original_output,
165
+ refined_output,
166
+ token_info,
167
+ cost_info
168
+ ]
169
+ )
170
+
171
+ # 啟動應用程式
172
+ demo.launch()
elevenlabs_stt.py ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 核心依賴
2
+ import requests
3
+ from requests.adapters import HTTPAdapter
4
+ from urllib3.util.retry import Retry
5
+ from typing import Optional, Dict, Any
6
+ import ssl
7
+ import logging
8
+ from elevenlabs.client import ElevenLabs
9
+ from io import BytesIO
10
+ import time
11
+
12
+
13
+ # 設定日誌記錄
14
+ logging.basicConfig(level=logging.INFO)
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class TLSAdapter(HTTPAdapter):
19
+ """自定義 TLS 適配器解決 SSL 協議問題"""
20
+ def init_poolmanager(self, *args, **kwargs):
21
+ ctx = ssl.create_default_context()
22
+ ctx.set_ciphers('DEFAULT@SECLEVEL=1') # 降低安全等級以兼容舊協議
23
+ ctx.options |= ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 # 禁用不安全的 SSL 版本
24
+ kwargs['ssl_context'] = ctx
25
+ return super().init_poolmanager(*args, **kwargs)
26
+
27
+
28
+ def create_retry_session():
29
+ """建立具有重試機制的 Session"""
30
+ session = requests.Session()
31
+ retry = Retry(
32
+ total=5, # 總重試次數
33
+ backoff_factor=1, # 重試間隔
34
+ status_forcelist=[500, 502, 503, 504], # 需要重試的狀態碼
35
+ allowed_methods=["POST"] # 只重試 POST 請求
36
+ )
37
+ adapter = HTTPAdapter(max_retries=retry)
38
+ session.mount("https://", adapter)
39
+ return session
40
+
41
+
42
+ def transcribe_audio(
43
+ api_key: str,
44
+ file_path: str,
45
+ language_code: Optional[str] = None,
46
+ diarize: bool = False,
47
+ max_retries: int = 5,
48
+ timeout: int = 600 # 10 分鐘超時
49
+ ) -> Optional[Dict[str, Any]]:
50
+ """
51
+ 使用 ElevenLabs API 將音訊轉換為文字,包含重試機制
52
+
53
+ Args:
54
+ api_key: ElevenLabs API 金鑰
55
+ file_path: 音訊檔案路徑
56
+ language_code: 語言代碼(可選,使用 ISO-639-1 或 ISO-639-3 格式)
57
+ diarize: 是否啟用說話者辨識(限制音訊長度最長 8 分鐘)
58
+ max_retries: 最大重試次數
59
+ timeout: 請求超時時間(秒)
60
+ """
61
+ # 初始化 ElevenLabs 客戶端
62
+ client = ElevenLabs(
63
+ api_key=api_key,
64
+ )
65
+
66
+ for attempt in range(max_retries):
67
+ try:
68
+ # 讀取音訊檔案
69
+ with open(file_path, 'rb') as audio_file:
70
+ audio_data = BytesIO(audio_file.read())
71
+
72
+ # 準備 API 參數
73
+ params = {
74
+ "file": audio_data,
75
+ "model_id": "scribe_v1",
76
+ "diarize": diarize,
77
+ "tag_audio_events": True,
78
+ "timestamps_granularity": "word"
79
+ }
80
+
81
+ # 只有當語言代碼不是 None 且不是空字串時才加入
82
+ if language_code and language_code.strip():
83
+ params["language_code"] = language_code.strip()
84
+
85
+ # 呼叫語音轉文字 API
86
+ response = client.speech_to_text.convert(**params)
87
+
88
+ # 檢查回應格式
89
+ if hasattr(response, 'text'):
90
+ language_code = getattr(
91
+ response, 'language_code', None
92
+ )
93
+ language_prob = getattr(
94
+ response, 'language_probability', None
95
+ )
96
+ return {
97
+ 'text': response.text,
98
+ 'language_code': language_code,
99
+ 'language_probability': language_prob
100
+ }
101
+ return response
102
+
103
+ except Exception as e:
104
+ logger.error(f"第 {attempt + 1} 次嘗試失敗:{str(e)}")
105
+ if attempt < max_retries - 1:
106
+ wait_time = min((attempt + 1) * 5, 30) # 最長等待 30 秒
107
+ logger.info(f"{wait_time} 秒後重試...")
108
+ time.sleep(wait_time)
109
+ else:
110
+ logger.error("已達最大重試次數,轉換失敗")
111
+ return None
112
+
113
+ # Example usage:
114
+ # transcription = transcribe_audio(
115
+ # api_key="YOUR_API_KEY",
116
+ # file_path="audio.mp3",
117
+ # language_code="en",
118
+ # diarize=True
119
+ # )
main_app.py ADDED
@@ -0,0 +1,384 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ from dotenv import load_dotenv
3
+ import os
4
+ from elevenlabs_stt import transcribe_audio as transcribe_audio_elevenlabs
5
+ from whisper_stt import transcribe_audio_whisper, get_available_models, get_model_description
6
+ from transcript_refiner import refine_transcript, OPENAI_MODELS
7
+ from utils import check_file_size, split_large_audio
8
+ import logging
9
+
10
+ # 載入環境變數
11
+ load_dotenv()
12
+
13
+ # 設定日誌
14
+ logging.basicConfig(level=logging.INFO)
15
+ logger = logging.getLogger(__name__)
16
+
17
+ # 定義可用的 OpenAI 模型
18
+ OPENAI_MODELS = {
19
+ "gpt-4o": "gpt-4o",
20
+ "gpt-4o-mini": "gpt-4o-mini",
21
+ "o3-mini": "o3-mini",
22
+ "o1-mini": "o1-mini"
23
+ }
24
+
25
+ # 模型設定和價格(USD per 1M tokens)
26
+ MODEL_CONFIG = {
27
+ "gpt-4o": {
28
+ "display_name": "gpt-4o",
29
+ "input": 2.50, # $2.50 per 1M tokens
30
+ "cached_input": 1.25, # $1.25 per 1M tokens
31
+ "output": 10.00 # $10.00 per 1M tokens
32
+ },
33
+ "gpt-4o-mini": {
34
+ "display_name": "gpt-4o-mini",
35
+ "input": 0.15, # $0.15 per 1M tokens
36
+ "cached_input": 0.075,# $0.075 per 1M tokens
37
+ "output": 0.60 # $0.60 per 1M tokens
38
+ },
39
+ "o1-mini": {
40
+ "display_name": "o1-mini",
41
+ "input": 1.10, # $1.10 per 1M tokens
42
+ "cached_input": 0.55, # $0.55 per 1M tokens
43
+ "output": 4.40 # $4.40 per 1M tokens
44
+ },
45
+ "o3-mini": {
46
+ "display_name": "o3-mini",
47
+ "input": 1.10, # $1.10 per 1M tokens
48
+ "cached_input": 0.55, # $0.55 per 1M tokens
49
+ "output": 4.40 # $4.40 per 1M tokens
50
+ }
51
+ }
52
+
53
+ # 匯率設定
54
+ USD_TO_NTD = 31.5
55
+
56
+ def calculate_cost(input_tokens, output_tokens, model_name, is_cached=False):
57
+ """計算 API 使用成本
58
+
59
+ Args:
60
+ input_tokens (int): 輸入 tokens 數量
61
+ output_tokens (int): 輸出 tokens 數量
62
+ model_name (str): 模型名稱 (gpt-4o, gpt-4o-mini, o1-mini, o3-mini)
63
+ is_cached (bool, optional): 是否使用快取輸入價格. 預設為 False
64
+
65
+ Returns:
66
+ tuple: (USD 成本, NTD 成本, 詳細計算資訊)
67
+ """
68
+ if model_name not in MODEL_CONFIG:
69
+ return 0, 0, "未支援的模型"
70
+
71
+ # 取得價格設定
72
+ model = MODEL_CONFIG[model_name]
73
+ input_price = model["cached_input"] if is_cached else model["input"]
74
+ output_price = model["output"]
75
+
76
+ # 計算 USD 成本 (以每 1M tokens 為單位)
77
+ input_cost = (input_tokens / 1_000_000) * input_price
78
+ output_cost = (output_tokens / 1_000_000) * output_price
79
+ total_cost_usd = input_cost + output_cost
80
+ total_cost_ntd = total_cost_usd * USD_TO_NTD
81
+
82
+ # 準備詳細計算資訊
83
+ details = f"""
84
+ 計算明細 (USD):
85
+ - 輸入: {input_tokens:,} tokens × ${input_price}/1M = ${input_cost:.4f}
86
+ - 輸出: {output_tokens:,} tokens × ${output_price}/1M = ${output_cost:.4f}
87
+ - 總計 (USD): ${total_cost_usd:.4f}
88
+ - 總計 (NTD): NT${total_cost_ntd:.2f}
89
+ """
90
+ return total_cost_usd, total_cost_ntd, details
91
+
92
+ # 在 Streamlit 介面中顯示成本
93
+ def display_cost_info(input_tokens, output_tokens, model_name, is_cached=False):
94
+ """在 Streamlit 介面中顯示成本資訊"""
95
+ cost_usd, cost_ntd, details = calculate_cost(
96
+ input_tokens,
97
+ output_tokens,
98
+ model_name,
99
+ is_cached
100
+ )
101
+
102
+ with st.sidebar.expander("💰 成本計算", expanded=True):
103
+ st.write("### Token 使用量")
104
+ st.write(f"- 輸入: {input_tokens:,} tokens")
105
+ st.write(f"- 輸出: {output_tokens:,} tokens")
106
+ st.write(f"- 總計: {input_tokens + output_tokens:,} tokens")
107
+
108
+ if (input_tokens + output_tokens) == 0:
109
+ st.warning("目前 token 使用量為 0,請確認是否已正確計算 token 數量!")
110
+
111
+ st.write("### 費用明細")
112
+ st.text(details)
113
+
114
+ if is_cached:
115
+ st.info("✨ 使用快取價格計算")
116
+
117
+ def main():
118
+ st.title("音訊轉文字與優化系統")
119
+
120
+ # 初始化 token 計數
121
+ if "input_tokens" not in st.session_state:
122
+ st.session_state.input_tokens = 0
123
+ if "output_tokens" not in st.session_state:
124
+ st.session_state.output_tokens = 0
125
+ if "total_tokens" not in st.session_state:
126
+ st.session_state.total_tokens = 0
127
+
128
+ # 檢查 session_state 中的 openai_model 是否有效,不是則重設為預設值 o3-mini
129
+ valid_openai_models = ["o3-mini", "o1-mini"]
130
+ if "openai_model" not in st.session_state or st.session_state["openai_model"] not in valid_openai_models:
131
+ st.session_state["openai_model"] = "o3-mini"
132
+ if "whisper_model" not in st.session_state:
133
+ st.session_state["whisper_model"] = "small"
134
+
135
+ with st.sidebar:
136
+ st.header("設定")
137
+
138
+ # 選擇轉錄服務
139
+ transcription_service = st.selectbox(
140
+ "選擇轉錄服務",
141
+ ["Whisper", "ElevenLabs"],
142
+ index=0,
143
+ help="選���要使用的語音轉文字服務"
144
+ )
145
+
146
+ # Whisper 相關設定
147
+ if transcription_service == "Whisper":
148
+ whisper_model = st.selectbox(
149
+ "選擇 Whisper 模型",
150
+ options=["tiny", "base", "small", "medium", "large"],
151
+ index=2 # 預設是 small (第三個選項)
152
+ )
153
+ st.session_state["whisper_model"] = whisper_model
154
+ st.caption(get_model_description(whisper_model))
155
+
156
+ # 語言設定
157
+ language_mode = st.radio(
158
+ "語言設定",
159
+ options=["自動偵測", "指定語言", "混合語言"],
160
+ help="選擇音訊的語言處理模式"
161
+ )
162
+
163
+ if language_mode == "指定語言":
164
+ languages = {
165
+ "中文 (繁體/簡體)": "zh",
166
+ "英文": "en",
167
+ "日文": "ja",
168
+ "韓文": "ko",
169
+ "其他": "custom"
170
+ }
171
+
172
+ selected_lang = st.selectbox(
173
+ "選擇語言",
174
+ options=list(languages.keys())
175
+ )
176
+
177
+ if selected_lang == "其他":
178
+ custom_lang = st.text_input(
179
+ "輸入語言代碼",
180
+ placeholder="例如:fr 代表法文",
181
+ help="請輸入 ISO 639-1 語言代碼"
182
+ )
183
+ language_code = custom_lang if custom_lang else None
184
+ else:
185
+ language_code = languages[selected_lang]
186
+ else:
187
+ language_code = None
188
+
189
+ # ElevenLabs 相關設定
190
+ elevenlabs_api_key = None
191
+ if transcription_service == "ElevenLabs":
192
+ elevenlabs_api_key = st.text_input(
193
+ "ElevenLabs API 金鑰",
194
+ type="password"
195
+ )
196
+
197
+ # OpenAI API 金鑰和模型選擇
198
+ openai_api_key = st.text_input(
199
+ "OpenAI API 金鑰",
200
+ type="password"
201
+ )
202
+
203
+ model_choice = st.selectbox(
204
+ "選擇 OpenAI 模型",
205
+ options=["gpt-4o", "gpt-4o-mini", "o1-mini", "o3-mini"],
206
+ index=3, # 預設選擇 o3-mini
207
+ help="選擇要使用的 OpenAI 模型"
208
+ )
209
+ st.session_state["openai_model"] = model_choice
210
+
211
+ # 其他設定
212
+ enable_diarization = st.checkbox("啟用說話者辨識", value=False)
213
+ temperature = st.slider("創意程度", 0.0, 1.0, 0.5)
214
+
215
+ # 作者資訊
216
+ st.markdown("---")
217
+ st.markdown("""
218
+ ### Created by
219
+ **Tseng Yao Hsien**
220
+ Endocrinologist
221
+ Tungs' Taichung MetroHarbor Hospital
222
+ """)
223
+
224
+ # 顯示價格說明
225
+ with st.sidebar.expander("💡 模型價格說明(USD per 1M tokens)"):
226
+ st.write("""
227
+ ### gpt-4o
228
+ - 輸入:$2.50 / 1M tokens
229
+ - 快取輸入:$1.25 / 1M tokens
230
+ - 輸出:$10.00 / 1M tokens
231
+
232
+ ### gpt-4o-mini
233
+ - 輸入:$0.15 / 1M tokens
234
+ - 快取輸入:$0.075 / 1M tokens
235
+ - 輸出:$0.60 / 1M tokens
236
+
237
+ ### o1-mini & o3-mini
238
+ - 輸入:$1.10 / 1M tokens
239
+ - 快取輸入:$0.55 / 1M tokens
240
+ - 輸出:$4.40 / 1M tokens
241
+
242
+ ### 匯率
243
+ - 1 USD = 31.5 NTD
244
+ """)
245
+
246
+ # 提示詞設定
247
+ with st.expander("提示詞設定(選填)", expanded=False):
248
+ context_prompt = st.text_area(
249
+ "請輸入相關提示詞",
250
+ placeholder="例如:\n- 這是一段醫學演講\n- 包含專有名詞:糖尿病、胰島素\n- 主要討論糖尿病的治療方法",
251
+ help="提供音訊內容的相關資訊,可以幫助 AI 更準確地理解和轉錄內容"
252
+ )
253
+
254
+ # 上傳檔案
255
+ uploaded_file = st.file_uploader("上傳音訊檔案", type=["mp3", "wav", "ogg", "m4a"])
256
+
257
+ if uploaded_file and st.button("處理音訊"):
258
+ if not openai_api_key:
259
+ st.error("請提供 OpenAI API 金鑰")
260
+ return
261
+
262
+ if transcription_service == "ElevenLabs" and not elevenlabs_api_key:
263
+ st.error("請提供 ElevenLabs API 金鑰")
264
+ return
265
+
266
+ try:
267
+ with st.spinner("處理中..."):
268
+ # 初始化變數
269
+ full_transcript = ""
270
+
271
+ # 檢查檔案大小
272
+ temp_path = f"temp_{uploaded_file.name}"
273
+ with open(temp_path, "wb") as f:
274
+ f.write(uploaded_file.getbuffer())
275
+
276
+ if check_file_size(temp_path):
277
+ # 檔案需要分割
278
+ audio_segments = split_large_audio(temp_path)
279
+ if not audio_segments:
280
+ st.error("檔案分割失敗")
281
+ return
282
+
283
+ progress_bar = st.progress(0)
284
+ for i, segment_path in enumerate(audio_segments):
285
+ if transcription_service == "Whisper":
286
+ result = transcribe_audio_whisper(
287
+ segment_path,
288
+ model_name=whisper_model,
289
+ language=language_code,
290
+ initial_prompt=context_prompt
291
+ )
292
+ else:
293
+ result = transcribe_audio_elevenlabs(
294
+ api_key=elevenlabs_api_key,
295
+ file_path=segment_path,
296
+ diarize=enable_diarization
297
+ )
298
+
299
+ if result:
300
+ full_transcript += result["text"] + "\n"
301
+ progress_bar.progress((i + 1) / len(audio_segments))
302
+ os.remove(segment_path)
303
+ else:
304
+ # 直接轉錄
305
+ if transcription_service == "Whisper":
306
+ result = transcribe_audio_whisper(
307
+ temp_path,
308
+ model_name=whisper_model,
309
+ language=language_code,
310
+ initial_prompt=context_prompt
311
+ )
312
+ else:
313
+ result = transcribe_audio_elevenlabs(
314
+ api_key=elevenlabs_api_key,
315
+ file_path=temp_path,
316
+ diarize=enable_diarization
317
+ )
318
+
319
+ if result:
320
+ full_transcript = result["text"]
321
+
322
+ # 清理原始暫存檔
323
+ os.remove(temp_path)
324
+
325
+ # 處理轉錄結果
326
+ if full_transcript:
327
+ st.subheader("原始轉錄文字")
328
+ st.text_area("原始文字", full_transcript, height=200)
329
+
330
+ # 優化文字
331
+ refined = refine_transcript(
332
+ raw_text=full_transcript,
333
+ api_key=openai_api_key,
334
+ model=model_choice,
335
+ temperature=temperature,
336
+ context=context_prompt
337
+ )
338
+
339
+ if refined:
340
+ st.subheader("優化後的文字")
341
+ st.text_area("修正後的文字", refined["corrected"], height=200)
342
+ st.subheader("文字摘要")
343
+ st.text_area("摘要", refined["summary"], height=200)
344
+
345
+ # 更新 token 使用統計(包含兩次 API 呼叫的總和)
346
+ current_usage = refined.get("usage", {})
347
+ st.session_state.input_tokens = current_usage.get("total_input_tokens", 0)
348
+ st.session_state.output_tokens = current_usage.get("total_output_tokens", 0)
349
+ st.session_state.total_tokens = st.session_state.input_tokens + st.session_state.output_tokens
350
+
351
+ # 顯示費用統計
352
+ st.markdown("---")
353
+ st.markdown("### 💰 費用統計")
354
+ st.markdown("#### 總計")
355
+ st.markdown(f"總 Tokens: **{st.session_state.total_tokens:,}**")
356
+
357
+ # 計算費用
358
+ total_cost_usd, total_cost_ntd, details = calculate_cost(
359
+ st.session_state.input_tokens,
360
+ st.session_state.output_tokens,
361
+ model_choice,
362
+ is_cached=False
363
+ )
364
+
365
+ st.markdown(f"總費用: **NT$ {total_cost_ntd:.2f}**")
366
+
367
+ # 顯示詳細成本資訊
368
+ display_cost_info(
369
+ st.session_state.input_tokens,
370
+ st.session_state.output_tokens,
371
+ model_choice,
372
+ is_cached=False
373
+ )
374
+ else:
375
+ st.error("文字優化失敗")
376
+ else:
377
+ st.error("轉錄失敗")
378
+
379
+ except Exception as e:
380
+ st.error(f"處理失敗:{str(e)}")
381
+ logger.error(f"處理失敗:{str(e)}")
382
+
383
+ if __name__ == "__main__":
384
+ main()
packages.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ ffmpeg
2
+ python3-pip
requirements.txt ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Core dependencies
2
+ elevenlabs>=1.0.0
3
+ openai>=1.0.0
4
+ gradio>=4.19.2
5
+ python-dotenv>=1.0.0
6
+ requests>=2.31.0
7
+
8
+ # Audio processing
9
+ pydub>=0.25.1
10
+ ffmpeg-python>=0.2.0
11
+ openai-whisper>=20231117
12
+ numpy>=1.24.0
13
+ torch>=2.0.0
14
+
15
+ # Networking and utilities
16
+ urllib3>=2.0.0
17
+ typing-extensions>=4.7.0
temp_podcast_testo_TRAVERSE.mp3 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:22c280d919c168d9efe0fd7ece7a46b9b464f4e34926b938e2e044aef15cabda
3
+ size 5816876
transcript_refiner.py ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from openai import OpenAI
2
+ from typing import Optional, Dict, Any
3
+ import streamlit as st
4
+
5
+ # 定義可用的 OpenAI 模型
6
+ OPENAI_MODELS = {
7
+ "gpt-4o": "GPT-4o",
8
+ "gpt-4o-mini": "GPT-4o-mini",
9
+ "o1-mini": "o1-mini",
10
+ "o3-mini": "o3-mini"
11
+ }
12
+
13
+ def refine_transcript(
14
+ raw_text: str,
15
+ api_key: str,
16
+ model: str = "o3-mini",
17
+ temperature: float = 0.5,
18
+ context: Optional[str] = None
19
+ ) -> Optional[Dict[str, Any]]:
20
+ """
21
+ 使用 OpenAI 優化轉錄文字
22
+
23
+ Args:
24
+ raw_text: 原始文字
25
+ api_key: OpenAI API 金鑰
26
+ model: 使用的模型名稱
27
+ temperature: 創意程度 (0.0-1.0)
28
+ context: 背景資訊
29
+ """
30
+ client = OpenAI(api_key=api_key)
31
+
32
+ try:
33
+ # 準備 API 參數
34
+ system_prompt = (
35
+ "你是一個專業的文字編輯,負責將文字轉換成正確的繁體中文並修正語法錯誤。"
36
+ "請保持原意,但確保輸出是優美的繁體中文。"
37
+ )
38
+ if context:
39
+ system_prompt += f"\n\n背景資訊:{context}"
40
+
41
+ params = {
42
+ "model": model,
43
+ "messages": [
44
+ {
45
+ "role": "system",
46
+ "content": system_prompt
47
+ },
48
+ {
49
+ "role": "user",
50
+ "content": f"請將以下文字轉換成繁體中文,並修正語法和標點符號:\n\n{raw_text}"
51
+ }
52
+ ]
53
+ }
54
+
55
+ # 只有 gpt-4o 和 gpt-4o-mini 支援 temperature
56
+ if model.startswith("gpt-4"):
57
+ params["temperature"] = temperature
58
+
59
+ # 第一步:修正並轉換為繁體中文
60
+ correction_response = client.chat.completions.create(**params)
61
+
62
+ corrected_text = correction_response.choices[0].message.content
63
+
64
+ # 第二步:結構化整理(使用相同的參數設定)
65
+ params["messages"] = [
66
+ {
67
+ "role": "system",
68
+ "content": (
69
+ "你是一個專業的文字編輯,負責整理和結構化文字內容。"
70
+ "請以繁體中文輸出,並確保格式清晰易讀。"
71
+ )
72
+ },
73
+ {
74
+ "role": "user",
75
+ "content": (
76
+ "請幫我整理以下文字,並提供:\n"
77
+ "1. 重點摘要\n"
78
+ "2. 關鍵字列表\n"
79
+ "3. 主要論點或重要資訊\n\n"
80
+ f"{corrected_text}"
81
+ )
82
+ }
83
+ ]
84
+
85
+ summary_response = client.chat.completions.create(**params)
86
+ summary_text = summary_response.choices[0].message.content
87
+
88
+ # 計算總 token 使用量
89
+ total_input_tokens = (
90
+ correction_response.usage.prompt_tokens +
91
+ summary_response.usage.prompt_tokens
92
+ )
93
+ total_output_tokens = (
94
+ correction_response.usage.completion_tokens +
95
+ summary_response.usage.completion_tokens
96
+ )
97
+
98
+ return {
99
+ "corrected": corrected_text,
100
+ "summary": summary_text,
101
+ "usage": {
102
+ "total_input_tokens": total_input_tokens,
103
+ "total_output_tokens": total_output_tokens,
104
+ "model": model
105
+ }
106
+ }
107
+
108
+ except Exception as e:
109
+ print(f"文字優化失敗:{str(e)}")
110
+ return None
111
+
112
+
113
+ def convert_to_traditional_chinese(
114
+ text: str,
115
+ api_key: str,
116
+ model: str = "o3-mini"
117
+ ) -> str:
118
+ """將文字轉換為繁體中文"""
119
+ client = OpenAI(api_key=api_key)
120
+
121
+ response = client.chat.completions.create(
122
+ model=model,
123
+ temperature=0.1, # 使用較低的溫度以確保準確轉換
124
+ messages=[
125
+ {
126
+ "role": "system",
127
+ "content": "你是一個專業的繁簡轉換工具,請將輸入文字轉換成繁體中文,保持原意不變。"
128
+ },
129
+ {
130
+ "role": "user",
131
+ "content": text
132
+ }
133
+ ]
134
+ )
135
+
136
+ return response.choices[0].message.content
137
+
138
+ # Example usage with elevenlabs_stt:
139
+ # raw_transcript = transcribe_audio(...)['text']
140
+ # refined = refine_transcript(
141
+ # raw_text=raw_transcript,
142
+ # api_key="OPENAI_API_KEY",
143
+ # temperature=0.5
144
+ # )
utils.py ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from typing import Tuple, List, Optional
3
+ from pydub import AudioSegment
4
+ import math
5
+ import logging
6
+
7
+ # 設定日誌
8
+ logging.basicConfig(level=logging.INFO)
9
+ logger = logging.getLogger(__name__)
10
+
11
+ # 常數定義
12
+ MAX_FILE_SIZE_MB = 25 # ElevenLabs 的檔案大小限制
13
+ SEGMENT_LENGTH_MS = 300000 # 5 分鐘,單位為毫秒
14
+
15
+ def check_file_constraints(file_path: str, diarize: bool = False) -> Tuple[bool, str]:
16
+ """檢查檔案限制條件"""
17
+ # 檔案大小限制 (25MB)
18
+ MAX_FILE_SIZE = 25 * 1024 * 1024
19
+ # 音訊長度限制(使用 diarize 時為 8 分鐘)
20
+ MAX_DURATION_DIARIZE = 8 * 60
21
+
22
+ try:
23
+ file_size = os.path.getsize(file_path)
24
+ if file_size > MAX_FILE_SIZE:
25
+ return False, f"檔案大小超過限制(最大 25MB):目前 {file_size/1024/1024:.1f}MB"
26
+
27
+ # 如果需要的話,這裡可以加入音訊長度檢查
28
+ # 需要安裝 pydub: pip install pydub
29
+ if diarize:
30
+ try:
31
+ audio = AudioSegment.from_file(file_path)
32
+ duration_seconds = len(audio) / 1000
33
+ if duration_seconds > MAX_DURATION_DIARIZE:
34
+ return False, (
35
+ f"使用說話者辨識時,音訊長度不能超過 8 分鐘:"
36
+ f"目前 {duration_seconds/60:.1f} 分鐘"
37
+ )
38
+ except ImportError:
39
+ pass # 如果沒有安裝 pydub,就跳過長度檢查
40
+
41
+ return True, "檔案檢查通過"
42
+ except Exception as e:
43
+ return False, f"檔案檢查失敗:{str(e)}"
44
+
45
+ def check_file_size(file_path: str, max_size_mb: int = MAX_FILE_SIZE_MB) -> bool:
46
+ """
47
+ 檢查檔案大小是否超過限制
48
+
49
+ Args:
50
+ file_path: 檔案路徑
51
+ max_size_mb: 最大檔案大小(MB)
52
+
53
+ Returns:
54
+ 如果檔案大小超過限制則返回 True
55
+ """
56
+ file_size_mb = os.path.getsize(file_path) / (1024 * 1024)
57
+ return file_size_mb > max_size_mb
58
+
59
+ def split_large_audio(file_path: str) -> Optional[List[str]]:
60
+ """
61
+ 將大型音訊檔案分割成較小的片段
62
+
63
+ Args:
64
+ file_path: 音訊檔案路徑
65
+
66
+ Returns:
67
+ 分割後的檔案路徑列表,如果失敗則返回 None
68
+ """
69
+ try:
70
+ # 載入音訊檔案
71
+ audio = AudioSegment.from_file(file_path)
72
+
73
+ # 如果檔案小於限制,直接返回原始檔案路徑
74
+ if not check_file_size(file_path):
75
+ return [file_path]
76
+
77
+ # 分割音訊
78
+ segments = []
79
+ for i, start in enumerate(range(0, len(audio), SEGMENT_LENGTH_MS)):
80
+ end = start + SEGMENT_LENGTH_MS
81
+ segment = audio[start:end]
82
+
83
+ # 儲存分割片段
84
+ segment_path = f"temp_segment_{i}.mp3"
85
+ segment.export(segment_path, format="mp3")
86
+ segments.append(segment_path)
87
+
88
+ logger.info(f"已建立分割片段:{segment_path}")
89
+
90
+ return segments
91
+
92
+ except Exception as e:
93
+ logger.error(f"分割音訊失敗:{str(e)}")
94
+ return None
whisper_stt.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import whisper
2
+ import logging
3
+ from typing import Optional, Dict, Any
4
+ import torch
5
+
6
+ # 設定日誌
7
+ logging.basicConfig(level=logging.INFO)
8
+ logger = logging.getLogger(__name__)
9
+
10
+ def transcribe_audio_whisper(
11
+ file_path: str,
12
+ model_name: str = "base",
13
+ language: Optional[str] = None,
14
+ initial_prompt: Optional[str] = None,
15
+ task: str = "transcribe"
16
+ ) -> Optional[Dict[str, Any]]:
17
+ """
18
+ 使用 Whisper 模型進行音訊轉文字
19
+
20
+ Args:
21
+ file_path: 音訊檔案路徑
22
+ model_name: Whisper 模型名稱 ("tiny", "base", "small", "medium", "large")
23
+ language: 音訊語言(ISO 639-1 代碼,如 "zh" 表示中文)
24
+ initial_prompt: 初始提示詞
25
+ task: 任務類型 ("transcribe" 或 "translate")
26
+
27
+ Returns:
28
+ 包含轉錄結果的字典,如果失敗則返回 None
29
+ """
30
+ try:
31
+ # 檢查 CUDA 是否可用
32
+ device = "cuda" if torch.cuda.is_available() else "cpu"
33
+ logger.info(f"使用設備: {device}")
34
+
35
+ # 載入模型
36
+ logger.info(f"載入 Whisper {model_name} 模型...")
37
+ model = whisper.load_model(model_name, device=device)
38
+
39
+ # 轉錄選項
40
+ options = {
41
+ "task": task,
42
+ "verbose": True
43
+ }
44
+ if language:
45
+ options["language"] = language
46
+ if initial_prompt:
47
+ options["initial_prompt"] = initial_prompt
48
+
49
+ # 執行轉錄
50
+ logger.info("開始轉錄...")
51
+ result = model.transcribe(file_path, **options)
52
+
53
+ # 整理結果
54
+ response = {
55
+ "text": result["text"],
56
+ "language": result.get("language", "unknown"),
57
+ "segments": result.get("segments", [])
58
+ }
59
+
60
+ logger.info("轉錄完成")
61
+ return response
62
+
63
+ except Exception as e:
64
+ logger.error(f"轉錄失敗:{str(e)}")
65
+ return None
66
+
67
+ def get_available_models() -> list:
68
+ """
69
+ 取得可用的 Whisper 模型列表
70
+ """
71
+ return ["tiny", "base", "small", "medium", "large"]
72
+
73
+ def get_model_description(model_name: str) -> str:
74
+ """
75
+ 取得模型描述
76
+ """
77
+ descriptions = {
78
+ "tiny": "最小的模型,速度最快但準確度較低",
79
+ "base": "基礎模型,平衡速度和準確度",
80
+ "small": "小型模型,準確度較好",
81
+ "medium": "中型模型,準確度高",
82
+ "large": "最大的模型,準確度最高但需要較多資源"
83
+ }
84
+ return descriptions.get(model_name, "未知模型")