diff --git a/README.md b/README.md index 84b90f7..a4912f7 100644 --- a/README.md +++ b/README.md @@ -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 ``` diff --git a/_.env b/_.env new file mode 100644 index 0000000..68a91a4 --- /dev/null +++ b/_.env @@ -0,0 +1,3 @@ +VOICEPEAK_PATH=/path/to/voicepeak +PROJECT_PATH=/path/to/project +ERROR_WEBHOOK_URL=https://your-webhook-url.com/endpoint \ No newline at end of file diff --git a/app/__init__.py b/app/__init__.py index 32d2b7f..2b1ac19 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -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 \ No newline at end of file diff --git a/app/errors.py b/app/errors.py new file mode 100644 index 0000000..a89f4bd --- /dev/null +++ b/app/errors.py @@ -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)) \ No newline at end of file diff --git a/app/routes.py b/app/routes.py index 4681826..787390d 100644 --- a/app/routes.py +++ b/app/routes.py @@ -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 \ No newline at end of file + 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") \ No newline at end of file diff --git a/app/webhook.py b/app/webhook.py new file mode 100644 index 0000000..31685e9 --- /dev/null +++ b/app/webhook.py @@ -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)}") \ No newline at end of file diff --git a/config.py b/config.py deleted file mode 100644 index 8de966c..0000000 --- a/config.py +++ /dev/null @@ -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 -} \ No newline at end of file diff --git a/config/__init__.py b/config/__init__.py new file mode 100644 index 0000000..3175c98 --- /dev/null +++ b/config/__init__.py @@ -0,0 +1,3 @@ +from .settings import get_config, Config, DevelopmentConfig, ProductionConfig + +__all__ = ['get_config', 'Config', 'DevelopmentConfig', 'ProductionConfig'] \ No newline at end of file diff --git a/config/settings.py b/config/settings.py new file mode 100644 index 0000000..23facfc --- /dev/null +++ b/config/settings.py @@ -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')] \ No newline at end of file diff --git a/manage_service.sh b/manage_service.sh deleted file mode 100644 index 3ab8306..0000000 --- a/manage_service.sh +++ /dev/null @@ -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 diff --git a/run_dev.sh b/run_dev.sh deleted file mode 100644 index e555483..0000000 --- a/run_dev.sh +++ /dev/null @@ -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 diff --git a/run_prod.sh b/run_prod.sh deleted file mode 100644 index 2dc6b5e..0000000 --- a/run_prod.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash - -source venv/bin/activate -gunicorn --workers 2 --bind 0.0.0.0:5000 wsgi:app \ No newline at end of file diff --git a/scripts/run_dev.sh b/scripts/run_dev.sh new file mode 100755 index 0000000..3447e6c --- /dev/null +++ b/scripts/run_dev.sh @@ -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 \ No newline at end of file diff --git a/scripts/run_prod.sh b/scripts/run_prod.sh new file mode 100755 index 0000000..91f2b48 --- /dev/null +++ b/scripts/run_prod.sh @@ -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 \ No newline at end of file diff --git a/_voicepeak-api.service b/services/_voicepeak-api.service similarity index 62% rename from _voicepeak-api.service rename to services/_voicepeak-api.service index 2c30831..bac6f04 100644 --- a/_voicepeak-api.service +++ b/services/_voicepeak-api.service @@ -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] diff --git a/voicepeak-api.service b/voicepeak-api.service deleted file mode 100644 index 2c12be4..0000000 --- a/voicepeak-api.service +++ /dev/null @@ -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 diff --git a/wsgi.py b/wsgi.py index bfc42ce..12d8ee4 100644 --- a/wsgi.py +++ b/wsgi.py @@ -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() \ No newline at end of file