Spaces:
Runtime error
Runtime error
| #!/usr/bin/env python3 | |
| # -*- coding: utf-8 -*- | |
| """ | |
| 語音錄音應用程式 | |
| 部署在 Hugging Face Spaces 上,從麥克風錄音並保存為 16kHz WAV 文件 | |
| Usage: | |
| python app.py | |
| """ | |
| import gradio as gr | |
| import numpy as np | |
| import wave | |
| import os | |
| from datetime import datetime | |
| from pathlib import Path | |
| # 設定 | |
| SAMPLE_RATE = 16000 # 16kHz | |
| OUTPUT_DIR = "recordings" # 錄音保存目錄 | |
| # 確保輸出目錄存在 | |
| os.makedirs(OUTPUT_DIR, exist_ok=True) | |
| def save_audio(audio_data: np.ndarray, sr: int) -> str: | |
| """ | |
| 將音頻數據保存為 WAV 文件 | |
| Args: | |
| audio_data: 音頻數據 (numpy array) | |
| sr: 採樣率 | |
| Returns: | |
| 保存的文件路徑 | |
| """ | |
| # 生成文件名 | |
| timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") | |
| filename = f"recording_{timestamp}.wav" | |
| filepath = os.path.join(OUTPUT_DIR, filename) | |
| # 確保是單聲道 | |
| if len(audio_data.shape) > 1: | |
| audio_data = audio_data.mean(axis=1) | |
| # 歸一化到 16-bit 範圍 | |
| audio_int16 = (audio_data * 32767).astype(np.int16) | |
| # 保存為 WAV | |
| with wave.open(filepath, 'w') as wav_file: | |
| wav_file.setnchannels(1) # 單聲道 | |
| wav_file.setsampwidth(2) # 2 bytes (16-bit) | |
| wav_file.setframerate(sr) | |
| wav_file.writeframes(audio_int16.tobytes()) | |
| return filepath | |
| def process_recording(audio_tuple): | |
| """ | |
| 處理錄音並保存 | |
| Args: | |
| audio_tuple: (sampling_rate, audio_data) 元組 | |
| Returns: | |
| 保存的文件路徑和狀態訊息 | |
| """ | |
| if audio_tuple is None: | |
| return None, "❌ 沒有錄音數據" | |
| sr, audio_data = audio_tuple | |
| # 如果採樣率不是 16kHz,需要重採樣 | |
| if sr != SAMPLE_RATE: | |
| # 使用 librosa 重採樣 | |
| try: | |
| import librosa | |
| audio_data = librosa.resample(audio_data, orig_sr=sr, target_sr=SAMPLE_RATE) | |
| sr = SAMPLE_RATE | |
| except ImportError: | |
| # 如果沒有 librosa,使用簡單的降採樣 | |
| ratio = SAMPLE_RATE / sr | |
| new_length = int(len(audio_data) * ratio) | |
| audio_data = np.interp( | |
| np.linspace(0, len(audio_data), new_length), | |
| np.arange(len(audio_data)), | |
| audio_data | |
| ) | |
| sr = SAMPLE_RATE | |
| # 保存文件 | |
| filepath = save_audio(audio_data, sr) | |
| # 計算錄音長度 | |
| duration = len(audio_data) / sr | |
| return filepath, f"✅ 已保存:{filepath}\n📊 長度:{duration:.2f}秒 | 採樣率:{sr}Hz" | |
| def create_demo(): | |
| """創建 Gradio 介面""" | |
| with gr.Blocks(title="語音錄音器") as demo: | |
| gr.Markdown(""" | |
| # 🎤 語音錄音器 | |
| 按下「開始錄音」按鈕開始錄音,再次按下「停止錄音」按鈕停止並保存。 | |
| 錄音將保存為 16kHz 採樣率的 WAV 文件。 | |
| """) | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| gr.Markdown("### 📝 錄音說明") | |
| gr.Markdown(""" | |
| 1. 點擊 **🔴 開始錄音** 按鈕 | |
| 2. 對著麥克風說話 | |
| 3. 點擊 **⏹️ 停止錄音** 按鈕 | |
| 4. 錄音將自動保存為 WAV 文件 | |
| """) | |
| gr.Markdown("### ⚙️ 設定") | |
| gr.Markdown(f""" | |
| - 採樣率:**{SAMPLE_RATE} Hz** | |
| - 格式:**WAV** | |
| - 聲道:**單聲道** | |
| - 位元深度:**16-bit** | |
| """) | |
| with gr.Column(scale=2): | |
| # 錄音組件 | |
| audio_input = gr.Audio( | |
| label="🎤 錄音", | |
| sources=["microphone"], | |
| type="numpy", | |
| waveform_options=gr.WaveformOptions( | |
| sample_rate=SAMPLE_RATE, | |
| ), | |
| ) | |
| # 狀態顯示 | |
| status_text = gr.Textbox( | |
| label="📋 狀態", | |
| placeholder="等待錄音...", | |
| interactive=False, | |
| ) | |
| # 保存的文件顯示 | |
| audio_output = gr.Audio( | |
| label="📁 已保存的錄音", | |
| type="filepath", | |
| interactive=False, | |
| ) | |
| # 按鈕 | |
| with gr.Row(): | |
| save_btn = gr.Button( | |
| "💾 保存錄音", | |
| variant="primary", | |
| size="lg", | |
| ) | |
| clear_btn = gr.Button( | |
| "🗑️ 清除", | |
| variant="secondary", | |
| size="lg", | |
| ) | |
| # 錄音說明 | |
| gr.Markdown(""" | |
| --- | |
| ### 💡 使用提示 | |
| - **錄音時**:請確保瀏覽器已授權麥克風權限 | |
| - **停止錄音**:錄音完成後點擊「保存錄音」按鈕 | |
| - **文件位置**:錄音保存在 `recordings/` 目錄下 | |
| - **下載**:點擊音頻播放器右側的下載按鈕可下載文件 | |
| """) | |
| # 事件處理 | |
| save_btn.click( | |
| fn=process_recording, | |
| inputs=[audio_input], | |
| outputs=[audio_output, status_text], | |
| ) | |
| clear_btn.click( | |
| fn=lambda: (None, "🗑️ 已清除"), | |
| inputs=[], | |
| outputs=[audio_input, status_text], | |
| ) | |
| return demo | |
| if __name__ == "__main__": | |
| # 創建並啟動應用 | |
| demo = create_demo() | |
| demo.launch() | |