Webhookでエラー通知、リファクタリング

This commit is contained in:
ntki 2024-08-27 00:21:25 +09:00
parent e3eac5581c
commit e279314af0
17 changed files with 239 additions and 99 deletions

View File

@ -4,18 +4,18 @@ PythonのFlaskフレームワークを使用し、Ubuntu上で動作します。
## 前提条件
- Linux Debian系OS
- 動作確認済み: Ubuntu24.04 LTS
- 動作確認済み: Ubuntu 24.04 LTS
- ライセンスがアクティベートされているVOICEPEAKがインストール済み
## セットアップ手順
### 1. リポジトリのクローン
```bash
git clone https://ntki-personal-web.access.ly/ntki72/voicepeak_api.git
cd voicepeak_api
```
### 2. Pythonの仮想環境を作成
仮想環境を作成し、必要なパッケージをインストールします。
```bash
# 仮想環境の作成
python3 -m venv venv
@ -27,15 +27,34 @@ source venv/bin/activate
pip install -r requirements.txt
```
### 3. Systemdサービスの設定
`_voicepeak-api.service`をコピーし`voicepeak-api.service`にリネームします。
`voicepeak-api.service`を開きServiceセクションのパラメーターを適切なものに書き換えます。
`voicepeak-api.service`ファイルをシステムのsystemdサービスディレクトリにシンボリックリンクを作成します。
### 3. 環境変数の設定
`.env` ファイルを作成し、必要な環境変数を設定します。
```bash
sudo ln -s /path/to/your/project/voicepeak-api.service /etc/systemd/system/voicepeak-api.service
cp _env .env
```
### 4. サービスのリロードと起動
`.env` ファイルを開き、以下の変数を適切に設定します:
- `VOICEPEAK_PATH`: VOICEPEAKの実行ファイルへのパス
- `PROJECT_PATH`: プロジェクトのルートディレクトリへのパス
- `ERROR_WEBHOOK_URL`: エラー通知用のDiscord WebhookのURL
### 4. Systemdサービスの設定
**注意**: 以前にこのサービスをインストールしたことがある場合は、まず「systemdサービスの削除」セクションの手順に従って古い設定を削除することをお勧めします。
`_voicepeak-api.service` ファイルをコピーし、必要に応じて編集します。
```bash
cp _voicepeak-api.service voicepeak-api.service
```
`voicepeak-api.service` ファイルを開き、`User`、`Group`、`WorkingDirectory`、`Environment` などのパラメータを適切に設定します。
設定が完了したら、システムのsystemdサービスディレクトリにシンボリックリンクを作成します。
```bash
sudo ln -s /path/to/your/project/services/voicepeak-api.service /etc/systemd/system/voicepeak-api.service
```
### 5. サービスのリロードと起動
systemdに新しいサービスを認識させ、サービスを起動します。
```bash
@ -49,7 +68,7 @@ sudo systemctl start voicepeak-api.service
sudo systemctl enable voicepeak-api.service
```
### 5. 起動確認
### 6. 起動確認
サービスが正常に起動しているか確認します。
```bash
sudo systemctl status voicepeak-api.service
@ -57,6 +76,62 @@ sudo systemctl status voicepeak-api.service
## 使用方法
サービスが正常に起動している場合、以下のようにリクエストを送信して音声を生成できます。
### 音声生成
```bash
curl -X POST http://127.0.0.1:5000/generate_voice -H "Content-Type: application/json" -d '{"text": "こんにちは"}' --output output.wav
curl -X POST http://localhost:15000/generate_voice \
-H "Content-Type: application/json" \
-d '{
"text": "こんにちは、テストです。",
"narrator": "SEKAI",
"emotion_happy": "0",
"emotion_sad": "0",
"emotion_angry": "0",
"emotion_fun": "0",
"pitch": "1"
}' \
--output test_output.wav
```
### エラーテスト
意図的にエラーを発生させてエラーハンドリングをテストするには:
```bash
curl http://localhost:15000/test_error
```
## トラブルシューティング
エラーが発生した場合、アプリケーションログを確認してください。また、設定したDiscord Webhookにもエラーメッセージが送信されていることを確認してください。
## 開発モードでの実行
```bash
./scripts/run_dev.sh
```
## systemdサービスの削除
プロジェクトを削除する場合や、サービスの設定をリセットする必要がある場合は、以下の手順でsystemdサービスを削除できます。
1. サービスを停止し、無効化します:
```bash
sudo systemctl stop voicepeak-api.service
sudo systemctl disable voicepeak-api.service
```
2. サービスファイルを systemd のディレクトリから削除します:
```bash
sudo rm /etc/systemd/system/voicepeak-api.service
```
3. シンボリックリンクを削除した場合、プロジェクトディレクトリ内のサービスファイルも削除できます:
```bash
rm /path/to/your/project/services/voicepeak-api.service
```
4. systemd の設定をリロードします:
```bash
sudo systemctl daemon-reload
```
5. systemd の設定から完全に削除されたことを確認します:
```bash
sudo systemctl reset-failed
```

3
_.env Normal file
View File

@ -0,0 +1,3 @@
VOICEPEAK_PATH=/path/to/voicepeak
PROJECT_PATH=/path/to/project
ERROR_WEBHOOK_URL=https://your-webhook-url.com/endpoint

View File

@ -1,12 +1,16 @@
from flask import Flask
from config import Config
from config import get_config
from .errors import register_error_handlers
def create_app(config_class=Config):
def create_app():
app = Flask(__name__)
app.config.from_object(config_class)
config_class.init_app(app)
config = get_config()
app.config.from_object(config)
config.init_app(app)
from app import routes
from . import routes
app.register_blueprint(routes.bp)
register_error_handlers(app) # Make sure this line is present
return app

17
app/errors.py Normal file
View File

@ -0,0 +1,17 @@
from flask import jsonify
import traceback
from .webhook import send_error_webhook
def handle_exception(app, error):
error_message = str(error)
error_type = type(error).__name__
stack_trace = traceback.format_exc()
app.logger.error(f'Unhandled Exception: {error_type} - {error_message}\n{stack_trace}')
send_error_webhook(error_message, error_type, stack_trace)
return jsonify(error=error_message), 500
def register_error_handlers(app):
app.register_error_handler(Exception, lambda e: handle_exception(app, e))

View File

@ -1,9 +1,10 @@
from flask import Blueprint, request, jsonify, send_file
from flask import Blueprint, request, jsonify, send_file, current_app
import subprocess
import os
import logging
import uuid
from flask import current_app
import traceback
from .webhook import send_error_webhook
bp = Blueprint('main', __name__)
@ -50,7 +51,7 @@ def generate_voice():
try:
data = request.json
text = data.get('text')
narrator = data.get('narrator', '1')
narrator = data.get('narrator', 'SEKAI')
emotion_happy = data.get('emotion_happy', '0')
emotion_sad = data.get('emotion_sad', '0')
emotion_angry = data.get('emotion_angry', '0')
@ -74,4 +75,24 @@ def generate_voice():
return response
except Exception as e:
return jsonify({'error': str(e)}), 500
error_message = str(e)
error_type = type(e).__name__
stack_trace = traceback.format_exc()
current_app.logger.error(f'Error in generate_voice: {error_message}')
current_app.logger.error(f'Traceback: {stack_trace}')
# Send webhook
send_error_webhook(error_message, error_type, stack_trace)
return jsonify({'error': error_message, 'traceback': stack_trace}), 500
@bp.route('/test_error')
def test_error():
current_app.logger.info("Testing error webhook")
raise Exception("This is a test error from routes.py")
@bp.route('/test_500_error')
def test_500_error():
current_app.logger.info("Testing 500 error webhook")
raise InternalServerError("This is a test 500 error from routes.py")

30
app/webhook.py Normal file
View File

@ -0,0 +1,30 @@
import requests
from flask import current_app
def send_error_webhook(error_message, error_type, stack_trace):
webhook_url = current_app.config.get('ERROR_WEBHOOK_URL')
if not webhook_url:
current_app.logger.warning("ERROR_WEBHOOK_URL not set. Skipping webhook.")
return
# エラーメッセージを整形
formatted_message = f"**Error Type:** {error_type}\n**Message:** {error_message}\n```\n{stack_trace[:1500]}...\n```"
# ディスコードのWebhook形式に合わせてペイロードを構築
payload = {
"embeds": [{
"title": "Error Details",
"description": formatted_message,
"color": 15158332 # 赤色
}]
}
try:
current_app.logger.debug(f"Sending webhook to URL: {webhook_url}")
current_app.logger.debug(f"Webhook payload: {payload}")
response = requests.post(webhook_url, json=payload)
response.raise_for_status()
current_app.logger.info(f"Error webhook sent successfully to Discord. Status code: {response.status_code}")
except requests.RequestException as e:
current_app.logger.error(f"Failed to send error webhook to Discord: {str(e)}")

View File

@ -1,30 +0,0 @@
import os
from dotenv import load_dotenv
load_dotenv()
class Config:
VOICEPEAK_PATH = os.getenv('VOICEPEAK_PATH')
PROJECT_PATH = os.getenv('PROJECT_PATH')
# 開発環境かどうかの判定
FLASK_ENV = os.getenv('FLASK_ENV', 'production')
@classmethod
def init_app(cls, app):
# Flaskアプリケーションのデバッグモードを設定
app.debug = cls.FLASK_ENV == 'development'
class DevelopmentConfig(Config):
# 開発環境時の設定: 今回は特になし
pass
class ProductionConfig(Config):
# 本番環境時の設定: 今回は特になし
pass
config = {
'development': DevelopmentConfig,
'production': ProductionConfig,
'default': Config
}

3
config/__init__.py Normal file
View File

@ -0,0 +1,3 @@
from .settings import get_config, Config, DevelopmentConfig, ProductionConfig
__all__ = ['get_config', 'Config', 'DevelopmentConfig', 'ProductionConfig']

32
config/settings.py Normal file
View File

@ -0,0 +1,32 @@
import os
from dotenv import load_dotenv
load_dotenv()
class Config:
VOICEPEAK_PATH = os.getenv('VOICEPEAK_PATH')
PROJECT_PATH = os.getenv('PROJECT_PATH')
ERROR_WEBHOOK_URL = os.getenv('ERROR_WEBHOOK_URL')
@classmethod
def init_app(cls, app):
app.logger.info(f"VOICEPEAK_PATH: {cls.VOICEPEAK_PATH}")
app.logger.info(f"PROJECT_PATH: {cls.PROJECT_PATH}")
app.logger.info(f"ERROR_WEBHOOK_URL: {'Set' if cls.ERROR_WEBHOOK_URL else 'Not set'}")
class DevelopmentConfig(Config):
DEBUG = True
ENV = 'development'
class ProductionConfig(Config):
DEBUG = False
ENV = 'production'
config = {
'development': DevelopmentConfig,
'production': ProductionConfig,
'default': ProductionConfig
}
def get_config():
return config[os.getenv('FLASK_ENV', 'production')]

View File

@ -1,19 +0,0 @@
#!/bin/bash
SERVICE_NAME="voicepeak-api.service"
# サービスの状態を確認
SERVICE_STATUS=$(systemctl is-active $SERVICE_NAME)
if [ "$SERVICE_STATUS" = "active" ]; then
echo "Restarting $SERVICE_NAME..."
sudo systemctl daemon-reload
sudo systemctl restart $SERVICE_NAME
else
echo "Reloading and starting $SERVICE_NAME..."
sudo systemctl daemon-reload
sudo systemctl start $SERVICE_NAME
fi
# サービスの状態を表示
sudo systemctl status $SERVICE_NAME

View File

@ -1,9 +0,0 @@
#!/bin/bash
# 仮想環境をアクティブにする
source venv/bin/activate
# Flaskアプリケーションを起動
export FLASK_APP=app
export FLASK_ENV=development
flask run --host=0.0.0.0 --port=5000

View File

@ -1,4 +0,0 @@
#!/bin/bash
source venv/bin/activate
gunicorn --workers 2 --bind 0.0.0.0:5000 wsgi:app

16
scripts/run_dev.sh Executable file
View File

@ -0,0 +1,16 @@
#!/bin/bash
# プロジェクトのルートディレクトリに移動
cd "$(dirname "$0")/.."
# 仮想環境が有効でない場合はアクティベート
if [[ -z "${VIRTUAL_ENV}" ]]; then
source venv/bin/activate
fi
# 環境変数を設定
export FLASK_ENV=development
export FLASK_APP=wsgi.py
# Flaskアプリケーションを起動
flask run --host=0.0.0.0 --port=5000

17
scripts/run_prod.sh Executable file
View File

@ -0,0 +1,17 @@
#!/bin/bash
# プロジェクトのルートディレクトリに移動
cd "$(dirname "$0")/.."
# 仮想環境が有効でない場合はアクティベート
if [[ -z "${VIRTUAL_ENV}" ]]; then
source venv/bin/activate
fi
# 環境変数を設定
export FLASK_ENV=production
export FLASK_APP=wsgi.py
# Gunicornでアプリケーションを起動
# NOTE: workersを1以上にするにはVOICEPEAKが1つ以上のプロセスを起動できないので実装を改善してエラーエラーが発生しないようにする必要がある
exec gunicorn --workers 1 --bind 0.0.0.0:5000 wsgi:app

View File

@ -7,7 +7,7 @@ User=your_username
Group=your_group
WorkingDirectory=/path/to/your/project
Environment="PATH=/path/to/your/project/venv/bin"
ExecStart=/path/to/your/project/run_prod.sh
ExecStart=/bin/bash -c 'source /path/to/your/project/venv/bin/activate && /path/to/your/project/scripts/run_prod.sh'
Restart=always
[Install]

View File

@ -1,15 +0,0 @@
[Unit]
Description=VoicePeak API Service
After=network.target
[Service]
User=ntki
Group=ntki
WorkingDirectory=/home/ntki/voicepeak_api
Environment="PATH=/home/ntki/voicepeak_api/venv/bin"
ExecStart=/home/ntki/voicepeak_api/venv/bin/python /home/ntki/voicepeak_api/app.py
EnvironmentFile=/home/ntki/voicepeak_api/.env
Restart=always
[Install]
WantedBy=multi-user.target

View File

@ -1,7 +1,6 @@
from app import create_app
from config import config
app = create_app(config['production'])
app = create_app()
if __name__ == "__main__":
app.run()