[備忘録] Pythonで作る音声入力対応Webページ - FastAPIを使った実装

## はじめに

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/929f3f44-387f-491f-a475-0adefdb4a2a8.png)

音声技術の進歩により、テキスト入力だけでなく音声による入力にも対応したウェブサイトやアプリケーションの需要が高まっています。この記事では、Python（FastAPI）とJavaScriptを使用して、簡単な音声入力に対応したウェブページを作成する方法をステップバイステップで解説します。FastAPIは高速で直感的なAPIの開発を可能にするモダンなPythonフレームワークです。

> **重要な注意事項:** 
> - この記事は**個人的な学習目的**での利用を想定しています。商用環境や実務での実装には追加的な検討が必要です。また、ここで紹介する手法だけでなく、より堅牢なアーキテクチャやセキュリティを備えた別の実装方法も検討すべきです。
> - ここで紹介する実装は**あくまでも実験的な試み**であり、音声入力の情報を秘密情報として扱わなければならないケースでは適切ではありません。個人情報や機密情報を含む可能性のある音声データを扱う場合は、適切なセキュリティ対策が必要です。

## 前提知識

- Pythonの基本的な知識
- HTMLとJavaScriptの基本
- ターミナル/コマンドライン操作の基本

## 必要なもの

- Python 3.6以上
- マイク付きのPC
- インターネット接続

## 環境構築

まずは必要なパッケージをインストールしましょう。

```bash
# 仮想環境を作成
python -m venv voice_env

# 仮想環境を有効化（Windows）
voice_env\Scripts\activate

# 仮想環境を有効化（Mac/Linux）
source voice_env/bin/activate

# 必要なパッケージをインストール
pip install fastapi uvicorn jinja2 python-multipart
```

## プロジェクト構成

以下のようなフォルダ構成でプロジェクトを作成します：

```
voice_input_app/
├── main.py
├── templates/
│   └── index.html
└── static/
    └── js/
        └── voice.js
```

## ステップ1: FastAPIアプリケーションの作成

![fastapi-architecture.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/83c6f3bb-3e56-4dc1-a0e1-ad988daeb46f.png)

`main.py`ファイルを作成し、以下のコードを記述します：

```python
from fastapi import FastAPI, Request, Body
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from pydantic import BaseModel
import uvicorn

# Pydanticモデルの定義
class AudioText(BaseModel):
    text: str

# FastAPIアプリケーションの初期化
app = FastAPI(title="音声入力アプリ")

# 静的ファイルとテンプレートの設定
app.mount("/static", StaticFiles(directory="static"), name="static")
templates = Jinja2Templates(directory="templates")

# ルートパスへのGETリクエストを処理
@app.get("/", response_class=HTMLResponse)
async def index(request: Request):
    return templates.TemplateResponse("index.html", {"request": request})

# 音声データを処理するエンドポイント
@app.post("/process_audio")
async def process_audio(audio_data: AudioText):
    # クライアントから送信された音声データの処理
    # このサンプルではクライアント側のWeb Speech APIを使用するため、
    # サーバー側での音声処理は行いません
    
    text = audio_data.text
    # ここで必要な処理を行う（例：データベースへの保存、分析など）
    
    response = {
        'status': 'success',
        'message': f'受信したテキスト: {text}',
        'processed_text': text.upper()  # 例として大文字に変換
    }
    
    return response

# 開発サーバー起動用（直接実行された場合）
if __name__ == "__main__":
    uvicorn.run("main:app", host="127.0.0.1", port=8000, reload=True)
```

## ステップ2: フロントエンドの作成

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/d59a7e7f-5688-479f-880a-67b05d975add.png)

`templates/index.html`ファイルを作成し、以下のコードを記述します：

```html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>音声入力デモ</title>
    <style>
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            max-width: 800px;
            margin: 0 auto;
            padding: 20px;
        }
        .container {
            background-color: #f5f5f5;
            padding: 20px;
            border-radius: 8px;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
        }
        button {
            background-color: #4CAF50;
            color: white;
            border: none;
            padding: 10px 20px;
            text-align: center;
            text-decoration: none;
            display: inline-block;
            font-size: 16px;
            margin: 10px 0;
            border-radius: 4px;
            cursor: pointer;
        }
        button:disabled {
            background-color: #cccccc;
            cursor: not-allowed;
        }
        #result {
            margin-top: 20px;
            min-height: 100px;
            border: 1px solid #ddd;
            padding: 10px;
            border-radius: 4px;
            background-color: white;
        }
        #status {
            color: #666;
            font-style: italic;
        }
        .disclaimer {
            background-color: #fffbf1;
            border-left: 4px solid #ffc107;
            padding: 10px 15px;
            margin: 20px 0;
            font-size: 14px;
            color: #856404;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>音声入力デモ</h1>
        
        <div class="disclaimer">
            <strong>注意:</strong> このデモは学習目的のみで提供されています。機密情報や個人情報の入力には使用しないでください。音声データはセキュアに保護されていない可能性があります。
        </div>
        
        <p>マイクボタンを押して話してみてください。</p>
        
        <button id="startButton">🎤 音声入力開始</button>
        <button id="stopButton" disabled>⏹ 停止</button>
        
        <div id="status">待機中...</div>
        
        <div id="result">
            <p>ここに音声認識の結果が表示されます</p>
        </div>
        
        <div id="serverResponse"></div>
    </div>

    <script src="/static/js/voice.js"></script>
</body>
</html>
```

## ステップ3: JavaScriptによる音声認識機能の実装

`static/js/voice.js`ファイルを作成し、以下のコードを記述します：

```javascript
document.addEventListener('DOMContentLoaded', function() {
    // 要素の取得
    const startButton = document.getElementById('startButton');
    const stopButton = document.getElementById('stopButton');
    const statusElement = document.getElementById('status');
    const resultElement = document.getElementById('result');
    const serverResponseElement = document.getElementById('serverResponse');
    
    // Web Speech APIのサポートチェック
    if (!('webkitSpeechRecognition' in window) && !('SpeechRecognition' in window)) {
        startButton.disabled = true;
        statusElement.textContent = 'このブラウザは音声認識をサポートしていません。Chrome または Edge をお試しください。';
        return;
    }
    
    // SpeechRecognitionオブジェクトの作成
    const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
    const recognition = new SpeechRecognition();
    
    // 設定
    recognition.lang = 'ja-JP';       // 言語を日本語に設定
    recognition.interimResults = true; // 途中結果も取得
    recognition.continuous = true;     // 連続認識モード
    
    let finalTranscript = '';
    
    // 音声認識開始ボタンのイベントハンドラ
    startButton.addEventListener('click', function() {
        finalTranscript = '';
        resultElement.innerHTML = '<p>認識中...</p>';
        recognition.start();
        
        startButton.disabled = true;
        stopButton.disabled = false;
        statusElement.textContent = '🎤 音声認識中...';
    });
    
    // 音声認識停止ボタンのイベントハンドラ
    stopButton.addEventListener('click', function() {
        recognition.stop();
        
        startButton.disabled = false;
        stopButton.disabled = true;
        statusElement.textContent = '停止しました';
        
        // 認識結果をサーバーに送信
        if (finalTranscript) {
            sendToServer(finalTranscript);
        }
    });
    
    // 音声認識の結果イベントハンドラ
    recognition.onresult = function(event) {
        let interimTranscript = '';
        
        for (let i = event.resultIndex; i < event.results.length; i++) {
            const transcript = event.results[i][0].transcript;
            
            if (event.results[i].isFinal) {
                finalTranscript += transcript;
            } else {
                interimTranscript += transcript;
            }
        }
        
        resultElement.innerHTML = `
            <p><strong>確定:</strong> ${finalTranscript}</p>
            <p><em>認識中:</em> ${interimTranscript}</p>
        `;
    };
    
    // エラーハンドラ
    recognition.onerror = function(event) {
        statusElement.textContent = `エラー: ${event.error}`;
        startButton.disabled = false;
        stopButton.disabled = true;
    };
    
    // 音声認識終了時のハンドラ
    recognition.onend = function() {
        statusElement.textContent = '音声認識が終了しました';
        startButton.disabled = false;
        stopButton.disabled = true;
    };
    
    // サーバーにデータを送信する関数
    function sendToServer(text) {
        statusElement.textContent = 'サーバーに送信中...';
        
        fetch('/process_audio', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({ text: text }),
        })
        .then(response => response.json())
        .then(data => {
            serverResponseElement.innerHTML = `
                <h3>サーバーからの応答:</h3>
                <pre>${JSON.stringify(data, null, 2)}</pre>
            `;
            statusElement.textContent = 'サーバー処理完了';
        })
        .catch(error => {
            console.error('Error:', error);
            serverResponseElement.innerHTML = `<p>エラー: ${error.message}</p>`;
            statusElement.textContent = 'サーバー通信エラー';
        });
    }
});
```

## 実行方法

プロジェクトフォルダ内で以下のコマンドを実行して、アプリケーションを起動します：

```bash
# 直接実行する場合
python main.py

# または uvicorn を使用する場合
uvicorn main:app --reload
```

動作画面の例 :
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/e34c3a4c-59ac-48e7-ac5b-dde72d948313.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/90433895-d0e1-4b6a-8a0c-3b7d8352ce18.png)


ブラウザで `http://127.0.0.1:8000` にアクセスすると、音声入力対応のWebページが表示されます。

FastAPIの特長として、自動生成されたAPIドキュメントを確認することもできます：
- Swagger UI: `http://127.0.0.1:8000/docs`
- ReDoc: `http://127.0.0.1:8000/redoc`

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/a809afa5-6c62-485f-bf22-55ddb5c09d92.png)

## 解説

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/c2de7145-49d8-48cc-a57f-1656fc6e175c.png)

このアプリケーションでは以下の技術を使用しています：

1. **サーバーサイド (Python/FastAPI)**
   - FastAPIフレームワークを使用してWebアプリケーションを構築
   - Pydanticモデルによる型安全なリクエスト/レスポンス処理
   - 自動APIドキュメント生成機能
   - 非同期処理による高いパフォーマンス

2. **クライアントサイド (HTML/JavaScript)**
   - Web Speech APIを使用してブラウザ上で音声認識を実行
   - 認識結果をサーバーに送信し、結果を表示

3. **音声認識の流れ**
   - ユーザーが「音声入力開始」ボタンをクリック
   - ブラウザがマイクへのアクセス許可を要求
   - 許可されるとマイクからの音声入力を認識し、テキストに変換
   - 「停止」ボタンをクリックすると、認識結果をサーバーに送信
   - サーバーは結果を処理し、応答を返す

## 発展させるためのアイデア

1. **データベース連携**
   - FastAPIとSQLAlchemyやTortoiseORMを使った音声認識結果の保存
   - OAuth2による認証機能の追加

2. **自然言語処理との統合**
   - spaCyやTransformersライブラリを使った音声コマンドの解析と実行機能
   - 感情分析や意図の検出

3. **テキスト読み上げ機能の追加**
   - Web Speech APIのSpeechSynthesisを使用
   - バックグラウンドタスクでの音声合成

4. **UIの改善**
   - 音声入力中の視覚的フィードバック
   - モバイル対応デザイン
   
5. **WebSocketを活用したリアルタイム機能**
   - FastAPIのWebSocketサポートを使用して、リアルタイムの音声認識結果を表示
   - 複数ユーザー間での音声チャット機能

## 注意点

- Web Speech APIはすべてのブラウザで対応しているわけではありません。Chrome, Edge, Safariなどの最新版で動作します。
- 高精度な音声認識にはインターネット接続が必要です。
- プライバシーに配慮し、マイクへのアクセス許可を明示的に要求することが重要です。
- **この実装は学習目的のみを想定しており、実際の業務や商用環境で使用する場合は、以下の点に注意が必要です：**
  - 音声データは機密情報として適切に保護する必要があります
  - 音声を通じて個人情報が送信される可能性がある場合は、適切なプライバシーポリシーとセキュリティ対策が必要です
  - 法的要件（GDPR、個人情報保護法など）に準拠した実装が必要です

## セキュリティに関する追加注意事項

この実装は実験的な試みとしての音声入力機能を紹介するものであり、セキュリティの観点からは以下の制限があります：

- データの暗号化を行っていないため、音声データや変換されたテキストは平文で送信されます
- 認証機能がないため、誰でもAPIにアクセスできる状態です
- 実際のアプリケーションでは、SSL/TLS (HTTPS) の使用が必須です
- 音声入力中のプライバシー表示やユーザーへの明確な通知が必要です

**本実装を拡張して実運用する場合は、セキュリティ専門家のレビューを受けることを強く推奨します。**

## FastAPIを使うメリット

1. **高速なパフォーマンス**
   - Starlette（ASGI）ベースで非同期処理をサポート
   - Uvicornとの組み合わせによる高速なレスポンス

2. **型安全性**
   - Pydanticによるデータバリデーションと型チェック
   - 開発時のエラー防止と自動補完サポート

3. **自動ドキュメント生成**
   - OpenAPI（Swagger）とReDocによるAPIドキュメントの自動生成
   - インタラクティブなAPIテスト環境

4. **拡張性**
   - 依存性注入システムによる柔軟なコード構成
   - WebSocketやバックグラウンドタスクのサポート

## まとめ

この記事では、FastAPIとJavaScriptを使用して音声入力に対応したWebページを作成する方法を紹介しました。Web Speech APIを使用することで、ブラウザ上で簡単に音声認識機能を実装でき、FastAPIを使うことで高速で拡張性の高いバックエンドを構築できることがわかりました。

この基本的な実装をベースに、FastAPIの強力な機能を活用して、より高度な機能を追加することで、様々な用途に対応できる音声入力アプリケーションを開発することができます。

## 代替手法の検討

実務や商用環境での実装を検討する場合、この記事で紹介したWeb Speech APIとFastAPIの組み合わせ以外にも、以下のような代替手法を検討することが重要です：

1. **クラウドベースの音声認識サービス**
   - Google Cloud Speech-to-Text
   - Amazon Transcribe
   - Microsoft Azure Speech Service
   - これらのサービスは高度なセキュリティ機能や、エンタープライズレベルの音声認識精度を提供します

2. **オンプレミスのソリューション**
   - Mozillaの「DeepSpeech」などのオープンソースモデルをローカルで実行
   - データをクラウドに送信せずに音声認識を行うため、より高いプライバシーを確保できます

3. **ハイブリッドアプローチ**
   - 簡単な音声コマンドはクライアントサイドで処理
   - 複雑な文章や重要なデータはセキュアなバックエンドで処理

4. **専用のSDKやフレームワーク**
   - 業界特化の音声認識SDKを使用（医療、法律、金融など）
   - 特定の用語や専門用語に対応した高精度な認識が可能

プロダクション環境では、これらの選択肢を機能要件、セキュリティ要件、予算、パフォーマンス要件などの観点から総合的に評価することをお勧めします。