├── .env.example ├── .gitignore ├── LICENSE ├── README.ja.md ├── README.md ├── app.py ├── docs ├── CLAUDE.md ├── architecture.md ├── assets │ └── avatar-ui_demo.gif ├── spec.md └── todo.md ├── requirements.txt ├── settings.py ├── static ├── css │ └── style.css ├── images │ ├── idle.png │ └── talk.png └── js │ ├── animation.js │ ├── app.js │ ├── chat.js │ ├── settings.js │ └── sound.js └── templates └── index.html /.env.example: -------------------------------------------------------------------------------- 1 | # =========================================== 2 | # 必須設定(これらがないと起動しません) 3 | # =========================================== 4 | 5 | # Google AI Studio APIキー 6 | # 取得先: https://aistudio.google.com/app/apikey 7 | GEMINI_API_KEY=your_api_key_here 8 | 9 | # 使用するGeminiモデル 10 | MODEL_NAME=gemini-2.0-flash 11 | 12 | # =========================================== 13 | # 任意設定(デフォルト値あり) 14 | # =========================================== 15 | 16 | # ===== サーバー設定 ===== 17 | # サーバーポート番号 18 | SERVER_PORT=5000 19 | 20 | # デバッグモード(開発時: True, 本番: False) 21 | DEBUG_MODE=True 22 | 23 | # ===== アバター設定 ===== 24 | # AIアシスタントの名前 25 | AVATAR_NAME=Spectra 26 | 27 | # AIアシスタントのフルネーム 28 | AVATAR_FULL_NAME=Spectra Communicator 29 | 30 | # アバター画像ファイル名(static/images/内のファイル) 31 | AVATAR_IMAGE_IDLE=idle.png 32 | AVATAR_IMAGE_TALK=talk.png 33 | 34 | # ===== AIシステムプロンプト設定 ===== 35 | # AIの人格や応答スタイル 36 | SYSTEM_INSTRUCTION=あなたはSpectraというAIアシスタントです。技術的で直接的なスタイルで簡潔に応答してください。回答は短く要点を押さえたものにしてください。 37 | 38 | # ===== UI設定 ===== 39 | # タイプライター効果の文字表示速度(ミリ秒) 40 | # 小さいほど高速 41 | TYPEWRITER_DELAY_MS=50 42 | 43 | # 口パクアニメーションの切替間隔(ミリ秒) 44 | MOUTH_ANIMATION_INTERVAL_MS=150 45 | 46 | # ===== サウンド設定 ===== 47 | # タイピング音の周波数(Hz) 48 | BEEP_FREQUENCY_HZ=800 49 | 50 | # タイピング音の長さ(ミリ秒) 51 | BEEP_DURATION_MS=50 52 | 53 | # タイピング音の音量(0.0-1.0) 54 | BEEP_VOLUME=0.05 55 | 56 | # タイピング音終了時の音量(フェードアウト用) 57 | BEEP_VOLUME_END=0.01 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | venv/ 6 | env/ 7 | .env 8 | 9 | # IDE 10 | .vscode/ 11 | .idea/ 12 | 13 | # OS 14 | .DS_Store 15 | Thumbs.db 16 | 17 | # Temporary files 18 | *.tmp 19 | *.swp 20 | *~ 21 | .env.backup 22 | 23 | # Project specific 24 | .claude/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Sito Sikino 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.ja.md: -------------------------------------------------------------------------------- 1 | # Avatar UI Core 2 | 3 | クラシックなターミナル調のUIコア。チャットUIからCLI統合まで拡張可能なプロジェクト基盤を提供します。 4 | 5 | ![License](https://img.shields.io/badge/license-MIT-blue.svg) 6 | ![Python](https://img.shields.io/badge/python-3.8%2B-blue.svg) 7 | ![Flask](https://img.shields.io/badge/flask-3.0.0-green.svg) 8 | 9 | ![Avatar UI Core Terminal Interface](docs/assets/avatar-ui_demo.gif) 10 | 11 | ## 特徴 12 | 13 | - **ターミナルUI** - グリーンオンブラックの古典的ターミナルインターフェース 14 | - **AIアバター** - 発話同期型のピクセルアートアバター表示 15 | - **タイプライター効果** - 文字単位のリアルタイム表示アニメーション 16 | - **サウンドエフェクト** - Web Audio APIによるタイピング音生成 17 | - **完全な設定管理** - すべての動作パラメータを`.env`ファイルで一元管理 18 | 19 | ## 基本操作 20 | 21 | 1. **メッセージ送信**: 画面下部の入力欄にテキストを入力してEnterキー 22 | 2. **会話履歴**: 自動的にスクロールされる会話履歴を確認 23 | 3. **アバター**: AIの応答中はアバターが応答アニメーション 24 | 25 | ## クイックスタート 26 | 27 | ### 必要要件 28 | 29 | - Python 3.8以上 30 | - Google AI Studio APIキー([取得はこちら](https://aistudio.google.com/app/apikey)) 31 | 32 | ### インストール手順 33 | 34 | #### 1. プロジェクトの取得 35 | 36 | ```bash 37 | # リポジトリのクローン(またはZIPダウンロード後に解凍) 38 | git clone https://github.com/yourusername/avatar-ui-core.git 39 | cd avatar-ui-core 40 | ``` 41 | 42 | #### 2. Python仮想環境の作成 43 | 44 | 仮想環境を使用することで、システムのPython環境を汚さずにプロジェクトを実行できます。 45 | 46 | ```bash 47 | # 仮想環境の作成 48 | python -m venv venv 49 | 50 | # 仮想環境の有効化 51 | # Linux/Mac: 52 | source venv/bin/activate 53 | # Windows (コマンドプロンプト): 54 | venv\Scripts\activate 55 | # Windows (PowerShell): 56 | venv\Scripts\Activate.ps1 57 | ``` 58 | 59 | 仮想環境が有効化されると、ターミナルのプロンプトに`(venv)`が表示されます。 60 | 61 | #### 3. 必要なパッケージのインストール 62 | 63 | ```bash 64 | # requirements.txtに記載されたパッケージを一括インストール 65 | pip install -r requirements.txt 66 | ``` 67 | 68 | ### 設定 69 | 70 | #### 1. 環境変数ファイルの準備 71 | 72 | ```bash 73 | # テンプレートファイルをコピーして.envファイルを作成 74 | cp .env.example .env 75 | # Windows: copy .env.example .env 76 | ``` 77 | 78 | #### 2. APIキーの設定 79 | 80 | テキストエディタで`.env`ファイルを開き、必須項目を設定: 81 | 82 | ```bash 83 | # 必須項目のみ変更が必要(他の項目はデフォルト値で動作) 84 | GEMINI_API_KEY=ここに取得したAPIキーを貼り付け 85 | MODEL_NAME=gemini-2.0-flash # または gemini-2.5-pro など 86 | ``` 87 | 88 | **重要**: `.env`ファイルには機密情報が含まれるため、絶対にGitにコミットしないでください。 89 | 90 | ### 起動 91 | 92 | ```bash 93 | # アプリケーションの起動 94 | python app.py 95 | ``` 96 | 97 | 起動に成功すると以下のようなメッセージが表示されます: 98 | ``` 99 | * Running on http://127.0.0.1:5000 100 | ``` 101 | 102 | ブラウザで `http://localhost:5000` にアクセスしてください。 103 | 104 | ## プロジェクト構造 105 | 106 | ``` 107 | avatar-ui-core/ 108 | ├── app.py # Flaskアプリケーション本体 109 | ├── settings.py # 設定管理モジュール 110 | ├── requirements.txt # Python依存関係 111 | ├── .env.example # 環境変数テンプレート 112 | ├── static/ 113 | │ ├── css/ 114 | │ │ └── style.css # UIスタイル定義 115 | │ ├── js/ 116 | │ │ ├── app.js # メインエントリーポイント 117 | │ │ ├── chat.js # チャット機能 118 | │ │ ├── animation.js # アニメーション制御 119 | │ │ ├── sound.js # 音響効果 120 | │ │ └── settings.js # フロントエンド設定 121 | │ └── images/ 122 | │ ├── idle.png # アバター(静止) 123 | │ └── talk.png # アバター(発話) 124 | └── templates/ 125 | └── index.html # HTMLテンプレート 126 | ``` 127 | 128 | **注意**: `docs/`フォルダには開発時のメモやアセットが含まれており、アプリケーション動作には影響しません。 129 | 130 | ## カスタマイズ方法 131 | 132 | すべての設定は`.env`ファイルで調整できます。 133 | 134 | ### 1. アバターの変更 135 | 136 | 画像ファイルを差し替える 137 | - `static/images/idle.png`: 静止時のアバター(推奨: 140x140px) 138 | - `static/images/talk.png`: 発話時のアバター(推奨: 140x140px) 139 | 140 | ### 2. AIの人格設定 141 | `.env`ファイルで以下の項目を編集: 142 | ```bash 143 | AVATAR_NAME=Spectra 144 | AVATAR_FULL_NAME=Spectra Communicator 145 | SYSTEM_INSTRUCTION=あなたはSpectraというAIアシスタントです。技術的で直接的なスタイルで簡潔に応答してください。回答は短く要点を押さえたものにしてください。 146 | ``` 147 | 148 | ### 3. UI動作の調整 149 | `.env`ファイルで各種速度を調整: 150 | ```bash 151 | # タイピング速度(ミリ秒、小さいほど高速) 152 | TYPEWRITER_DELAY_MS=30 153 | 154 | # 口パクアニメーション間隔(ミリ秒) 155 | MOUTH_ANIMATION_INTERVAL_MS=100 156 | ``` 157 | 158 | ### 4. サウンド設定 159 | `.env`ファイルで音響効果をカスタマイズ: 160 | ```bash 161 | BEEP_FREQUENCY_HZ=600 # 音の高さ(Hz) 162 | BEEP_VOLUME=0.1 # 音量(0.0-1.0) 163 | BEEP_DURATION_MS=30 # 音の長さ(ミリ秒) 164 | ``` 165 | 166 | **注意**: 設定変更後はアプリケーションの再起動が必要です。 167 | 168 | ## 環境変数一覧 169 | 170 | | 変数名 | 説明 | デフォルト値 | 必須 | 171 | |--------|------|-------------|------| 172 | | `GEMINI_API_KEY` | Google Gemini APIキー | - | ✅ | 173 | | `MODEL_NAME` | 使用するGeminiモデル | gemini-2.0-flash | ✅ | 174 | | **サーバー設定** | | | | 175 | | `SERVER_PORT` | サーバーポート番号 | 5000 | | 176 | | `DEBUG_MODE` | デバッグモード有効化 | True | | 177 | | **アバター設定** | | | | 178 | | `AVATAR_NAME` | AIアシスタントの名前 | Spectra | | 179 | | `AVATAR_FULL_NAME` | AIアシスタントのフルネーム | Spectra Communicator | | 180 | | `AVATAR_IMAGE_IDLE` | 静止時のアバター画像 | idle.png | | 181 | | `AVATAR_IMAGE_TALK` | 発話時のアバター画像 | talk.png | | 182 | | **AI性格設定** | | | | 183 | | `SYSTEM_INSTRUCTION` | AIの人格や応答スタイル | 技術的で簡潔な応答 | | 184 | | **UI設定** | | | | 185 | | `TYPEWRITER_DELAY_MS` | タイプライター効果の速度(ミリ秒) | 50 | | 186 | | `MOUTH_ANIMATION_INTERVAL_MS` | 口パクアニメーション間隔(ミリ秒) | 150 | | 187 | | **サウンド設定** | | | | 188 | | `BEEP_FREQUENCY_HZ` | ビープ音の周波数(Hz) | 800 | | 189 | | `BEEP_DURATION_MS` | ビープ音の長さ(ミリ秒) | 50 | | 190 | | `BEEP_VOLUME` | ビープ音の音量(0.0-1.0) | 0.05 | | 191 | | `BEEP_VOLUME_END` | ビープ音終了時の音量 | 0.01 | | 192 | 193 | ## 技術スタック 194 | 195 | ### バックエンド 196 | - **Flask 3.0.0** - Webアプリケーションフレームワーク 197 | - **google-generativeai 0.8.3** - Gemini API統合 198 | - **python-dotenv 1.0.0** - 環境変数管理 199 | 200 | ### フロントエンド 201 | - **ES6 Modules** - モジュール化されたJavaScript 202 | - **Web Audio API** - ブラウザネイティブ音響生成 203 | - **CSS3** - モダンなスタイリング 204 | - **Fira Code** - プログラミング用等幅フォント 205 | 206 | ## ⚠️ 注意事項 207 | 208 | - 本プロジェクトはネイキッド版UI基盤として提供されており、デフォルト実装は単一ユーザー利用を前提としています。 209 | - APIキーなどの秘密情報は `.env` に保存され、サーバー内でのみ利用されます。 210 | - 個人利用・学習用途ではそのままご利用いただけますが、不特定多数に公開する場合は 211 | - ユーザーごとの設定保存 212 | - 認証機構の追加 213 | 214 | が必須となります。 215 | 216 | ## ライセンス 217 | 218 | MIT License - 詳細は[LICENSE](LICENSE)ファイルを参照 219 | 220 | ## クレジット 221 | 222 | Developed by Sito Sikino 223 | 224 | ### 使用技術 225 | - Google Gemini API 226 | - Flask Framework 227 | - Fira Code Font 228 | 229 | --- 230 | 231 | **注意**: このプロジェクトはエンタメ・創作目的で作成されています。本番環境での使用時は適切なセキュリティ対策を実施してください。 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Avatar UI Core 2 | 3 |
4 | 5 | **[📖 日本語版はこちら](README.ja.md)** 6 | 7 |
8 | 9 | A classic terminal-style UI core. Provides an extensible project foundation from chat UI to CLI integration. 10 | 11 | ![License](https://img.shields.io/badge/license-MIT-blue.svg) 12 | ![Python](https://img.shields.io/badge/python-3.8%2B-blue.svg) 13 | ![Flask](https://img.shields.io/badge/flask-3.0.0-green.svg) 14 | 15 | ![Avatar UI Core Terminal Interface](docs/assets/avatar-ui_demo.gif) 16 | 17 | ## Features 18 | 19 | - **Terminal UI** - Classic green-on-black terminal interface 20 | - **AI Avatar** - Pixel art avatar with synchronized speech animation 21 | - **Typewriter Effect** - Real-time character-by-character display animation 22 | - **Sound Effects** - Typing sound generation using Web Audio API 23 | - **Complete Configuration Management** - All parameters managed via `.env` file 24 | 25 | ## Quick Start 26 | 27 | ### Requirements 28 | 29 | - Python 3.8 or higher (Check version: `python --version` or `python3 --version`) 30 | - pip (Python package manager, usually included with Python) 31 | - Google AI Studio API Key ([Get it here](https://aistudio.google.com/app/apikey)) 32 | 33 | ### Installation 34 | 35 | #### 1. Clone the Repository 36 | 37 | ```bash 38 | # Clone repository (or download and extract ZIP) 39 | git clone https://github.com/yourusername/avatar-ui-core.git 40 | cd avatar-ui-core 41 | ``` 42 | 43 | #### 2. Create Python Virtual Environment 44 | 45 | Using a virtual environment keeps your system Python clean. 46 | 47 | ```bash 48 | # Create virtual environment 49 | python -m venv venv 50 | 51 | # Activate virtual environment 52 | # Linux/Mac: 53 | source venv/bin/activate 54 | # Windows (Command Prompt): 55 | venv\Scripts\activate 56 | # Windows (PowerShell): 57 | venv\Scripts\Activate.ps1 58 | ``` 59 | 60 | When activated, you'll see `(venv)` in your terminal prompt. 61 | 62 | #### 3. Install Required Packages 63 | 64 | ```bash 65 | # Install packages from requirements.txt 66 | pip install -r requirements.txt 67 | ``` 68 | 69 | ### Configuration 70 | 71 | #### 1. Prepare Environment File 72 | 73 | ```bash 74 | # Copy template to create .env file 75 | cp .env.example .env 76 | # Windows: copy .env.example .env 77 | ``` 78 | 79 | #### 2. Set API Key 80 | 81 | Open `.env` file in a text editor and configure required settings: 82 | 83 | ```bash 84 | # Only required items need to be changed (others use default values) 85 | GEMINI_API_KEY=paste_your_api_key_here 86 | MODEL_NAME=gemini-2.0-flash # or gemini-1.5-pro etc. 87 | ``` 88 | 89 | Other settings (avatar name, UI speed, sound effects) have default values and work out of the box. You can customize them later as needed. 90 | 91 | **Important**: `.env` file contains sensitive information. Never commit it to Git. 92 | 93 | ### Launch 94 | 95 | ```bash 96 | # Start the application 97 | python app.py 98 | ``` 99 | 100 | On successful launch, you'll see: 101 | ``` 102 | * Running on http://127.0.0.1:5000 103 | ``` 104 | 105 | Access the application at `http://localhost:5000` in your browser. 106 | 107 | ## Usage 108 | 109 | ### Basic Operations 110 | 111 | 1. **Send Message**: Type text in the input field at the bottom and press Enter 112 | 2. **Chat History**: View automatically scrolling conversation history 113 | 3. **Avatar**: Avatar animates while AI is responding 114 | 115 | ## Developer Information 116 | 117 | ### Project Structure 118 | 119 | ``` 120 | avatar-ui-core/ 121 | ├── app.py # Flask application 122 | ├── settings.py # Configuration management 123 | ├── requirements.txt # Python dependencies 124 | ├── .env.example # Environment template 125 | ├── static/ 126 | │ ├── css/ 127 | │ │ └── style.css # UI styles 128 | │ ├── js/ 129 | │ │ ├── app.js # Main entry point 130 | │ │ ├── chat.js # Chat functionality 131 | │ │ ├── animation.js # Animation control 132 | │ │ ├── sound.js # Sound effects 133 | │ │ └── settings.js # Frontend configuration 134 | │ └── images/ 135 | │ ├── idle.png # Avatar (idle) 136 | │ └── talk.png # Avatar (talking) 137 | └── templates/ 138 | └── index.html # HTML template 139 | ``` 140 | 141 | **Note**: The `docs/` folder contains development notes and assets, and does not affect application functionality. 142 | 143 | ### API Endpoints 144 | 145 | | Endpoint | Method | Description | Parameters | Response | 146 | |----------|--------|-------------|------------|----------| 147 | | `/` | GET | Display main page | None | HTML | 148 | | `/api/chat` | POST | Chat with AI | `{message: string}` | `{response: string}` or `{error: string}` | 149 | 150 | ### Customization 151 | 152 | All settings can be adjusted in the `.env` file. 153 | 154 | #### 1. Change Avatar 155 | 156 | Replace image files: 157 | - `static/images/idle.png`: Idle avatar (recommended: 140x140px) 158 | - `static/images/talk.png`: Talking avatar (recommended: 140x140px) 159 | 160 | #### 2. AI Personality 161 | 162 | Edit these items in `.env` file: 163 | ```bash 164 | AVATAR_NAME=Spectra 165 | AVATAR_FULL_NAME=Spectra Communicator 166 | SYSTEM_INSTRUCTION=You are Spectra, an AI assistant. Respond in a technical and direct style with concise answers. 167 | ``` 168 | 169 | #### 3. UI Behavior 170 | 171 | Adjust various speeds in `.env` file: 172 | ```bash 173 | # Typing speed (milliseconds, smaller = faster) 174 | TYPEWRITER_DELAY_MS=30 175 | 176 | # Mouth animation interval (milliseconds) 177 | MOUTH_ANIMATION_INTERVAL_MS=100 178 | ``` 179 | 180 | #### 4. Sound Settings 181 | 182 | Customize sound effects in `.env` file: 183 | ```bash 184 | BEEP_FREQUENCY_HZ=600 # Pitch (Hz) 185 | BEEP_VOLUME=0.1 # Volume (0.0-1.0) 186 | BEEP_DURATION_MS=30 # Duration (milliseconds) 187 | ``` 188 | 189 | **Note**: Application restart required after configuration changes. 190 | 191 | ## Environment Variables 192 | 193 | | Variable | Description | Default | Required | 194 | |----------|-------------|---------|----------| 195 | | `GEMINI_API_KEY` | Google Gemini API Key | - | ✅ | 196 | | `MODEL_NAME` | Gemini model to use | gemini-2.0-flash | ✅ | 197 | | **Server Settings** | | | | 198 | | `SERVER_PORT` | Server port number | 5000 | | 199 | | `DEBUG_MODE` | Enable debug mode | True | | 200 | | **Avatar Settings** | | | | 201 | | `AVATAR_NAME` | AI assistant name | Spectra | | 202 | | `AVATAR_FULL_NAME` | AI assistant full name | Spectra Communicator | | 203 | | `AVATAR_IMAGE_IDLE` | Idle avatar image | idle.png | | 204 | | `AVATAR_IMAGE_TALK` | Talking avatar image | talk.png | | 205 | | **AI Personality** | | | | 206 | | `SYSTEM_INSTRUCTION` | AI personality and response style | Technical and concise responses | | 207 | | **UI Settings** | | | | 208 | | `TYPEWRITER_DELAY_MS` | Typewriter effect speed (ms) | 50 | | 209 | | `MOUTH_ANIMATION_INTERVAL_MS` | Mouth animation interval (ms) | 150 | | 210 | | **Sound Settings** | | | | 211 | | `BEEP_FREQUENCY_HZ` | Beep frequency (Hz) | 800 | | 212 | | `BEEP_DURATION_MS` | Beep duration (ms) | 50 | | 213 | | `BEEP_VOLUME` | Beep volume (0.0-1.0) | 0.05 | | 214 | | `BEEP_VOLUME_END` | Beep end volume | 0.01 | | 215 | 216 | ## Tech Stack 217 | 218 | ### Backend 219 | - **Flask 3.0.0** - Web application framework 220 | - **google-generativeai 0.8.3** - Gemini API integration 221 | - **python-dotenv 1.0.0** - Environment variable management 222 | 223 | ### Frontend 224 | - **ES6 Modules** - Modular JavaScript 225 | - **Web Audio API** - Native browser sound generation 226 | - **CSS3** - Modern styling 227 | - **Fira Code** - Programming font 228 | 229 | ## ⚠️ Important Notes 230 | 231 | - This project is provided as a naked UI foundation, with default implementation designed for single-user use. 232 | - Sensitive information such as API keys is stored in `.env` and used only on the server side. 233 | - Suitable for personal use and learning purposes as-is, but when deploying for public access: 234 | - User-specific configuration storage required 235 | - Authentication mechanisms must be added 236 | 237 | ## License 238 | 239 | MIT License - See [LICENSE](LICENSE) file for details 240 | 241 | ## Credits 242 | 243 | Developed by Sito Sikino 244 | 245 | ### Technologies Used 246 | - Google Gemini API 247 | - Flask Framework 248 | - Fira Code Font 249 | 250 | --- 251 | 252 | **Note**: This project is created for entertainment and creative purposes. Please implement appropriate security measures when using in production environments. -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | """Webアプリケーション""" 2 | from flask import Flask, render_template, request, jsonify 3 | import google.generativeai as genai 4 | import settings 5 | 6 | # Gemini API接続 7 | genai.configure(api_key=settings.GEMINI_API_KEY) 8 | model = genai.GenerativeModel( 9 | model_name=settings.MODEL_NAME, 10 | system_instruction=settings.SYSTEM_INSTRUCTION 11 | ) 12 | chat = model.start_chat() # チャットセッション開始 13 | 14 | app = Flask(__name__) 15 | 16 | @app.route('/') 17 | def index(): 18 | """メインページ表示""" 19 | config = { 20 | 'typewriter_delay': settings.TYPEWRITER_DELAY_MS, 21 | 'avatar_name': settings.AVATAR_NAME, 22 | 'avatar_full_name': settings.AVATAR_FULL_NAME, 23 | 'mouth_animation_interval': settings.MOUTH_ANIMATION_INTERVAL_MS, 24 | 'beep_frequency': settings.BEEP_FREQUENCY_HZ, 25 | 'beep_duration': settings.BEEP_DURATION_MS, 26 | 'beep_volume': settings.BEEP_VOLUME, 27 | 'beep_volume_end': settings.BEEP_VOLUME_END, 28 | 'avatar_image_idle': settings.AVATAR_IMAGE_IDLE, 29 | 'avatar_image_talk': settings.AVATAR_IMAGE_TALK 30 | } 31 | return render_template('index.html', config=config) 32 | 33 | @app.route('/api/chat', methods=['POST']) 34 | def api_chat(): 35 | """ユーザー入力を受信しAI応答を返す""" 36 | message = request.json['message'] 37 | response = chat.send_message(message) 38 | return jsonify({'response': response.text}) 39 | 40 | if __name__ == '__main__': 41 | app.run(debug=settings.DEBUG_MODE, port=settings.SERVER_PORT) -------------------------------------------------------------------------------- /docs/CLAUDE.md: -------------------------------------------------------------------------------- 1 | # CLAUDE.md - avatar-ui 2 | 3 | このファイルは、[Claude Code](https://www.anthropic.com/claude-code) がこのリポジトリのコードを扱う際のガイダンスを提供します。 4 | 5 | ## ドキュメント構成 6 | 7 | - `spec.md` - 要件定義 8 | - `architecture.md` - アーキテクチャ詳細 9 | 10 | ## プロジェクト概要 11 | 12 | 「avatar-ui」は、クラシックなターミナルスタイルのUIコア。チャットUIからCLI統合まで拡張可能なプロジェクト基盤。 13 | 14 | ## 🚨 **最重要事項** 15 | 16 | Claude Code は次の原則を**絶対基準**として実装に従う。コード・設定・命名・タスク処理すべてが、この基準を背骨として段階的に進む。 17 | 18 | ### 設定管理原則 19 | - **一元管理**:設定値は意味ごとに構造化し `settings.py` で一元管理 20 | - **環境対応**:環境依存値や機密情報はすべて `.env` に置き、`settings.py` で動的読み込み 21 | - **意味的実装**:コードは具体値ではなく意味名(例 `SPEECH_INTERVAL`)で制御 22 | - **ハードコード禁止**:具体値はすべて `.env` と `settings.py` 経由で注入することで動的に制御する 23 | - **venv必須**:Python仮想環境は必ず `venv` を使用し、依存関係を分離管理する 24 | 25 | ### 開発原則 26 | - **Fail-Fast**:すべてのエラーは即時に停止し、原因を表面化させる(Fail-Fast)。**すべての処理は例外時にもフォールバックせず、明示的に失敗させる設計**とする。 27 | - **最小実装**:要求機能だけを実装し、余分なコードやファイルを加えない 28 | - **TDD採用**:t-wada式 Red→Green→Refactor→Commit を徹底 29 | 30 | ### コード品質 31 | - **本質性**:不要ファイル・冗長コードを排除 32 | - **日本語コメント**:本質的な意図のみを簡潔に日本語で記述 33 | - **命名**:Uncle Bob の Intention-Revealing Names に従う 34 | 35 | --- 36 | 37 | ## Phase 1 EXPLORE(調査) 38 | 39 | **目的** 40 | - 文脈と依存関係を把握し、実装前に全体像を理解する 41 | 42 | **行動** 43 | 1. Brave Search、公式ドキュメント、GitHub を一次情報源として調査 44 | 2. `.env`, `Dockerfile`, CI 設定、依存ライブラリ、`settings.py` を確認 45 | 3. Context7(mcp) に調査内容を蓄積・整理し、Claude(Use Subagent)で役割分担 46 | 4. Serena (mcp) を駆使し、文脈を正確に保持し続ける 47 | 5. 過去ログ `docs/dev-log/`(`yyyy-mm-dd_hh-mm_機能名.md`)を全読み込みし、既存の設計意図・副作用を把握 48 | 49 | --- 50 | 51 | ## Phase 2 PLAN(計画) 52 | 53 | **目的** 54 | - 実装を最小タスクへ分割し、受け入れ条件と運用ルールを明確にする 55 | 56 | **行動** 57 | 1. `todo.md` にマイクロタスクを列挙し、各タスクに受け入れ条件を付与 58 | 2. 各タスクを t-wada式 TDD サイクル 1回分として設計 59 | 3. `settings.py` に置く定数と `.env` の環境値を棚卸し 60 | 4. Context7 + Claude(Use Subagent) でタスク粒度・設計整合性をレビュー 61 | 5. Serena (mcp) を駆使し、文脈を正確に保持し続ける 62 | 63 | --- 64 | 65 | ## Phase 3 IMPLEMENT(実装) 66 | 67 | **ルール** 68 | - **ハードコード禁止**:具体値はすべて `.env` と `settings.py` 経由で注入することで動的に制御する 69 | - **Fail-Fast** を最上位原則とし、異常時は即停止 70 | - **フォールバック禁止**:いかなる例外も認めず、本来の目的コード以外の例外措置は施さない 71 | - **Serena (mcp)** を駆使し、文脈を正確に保持し続ける 72 | 73 | ### 🔴 Red — 失敗するテストを書く 74 | - 未実装状態で必ず失敗するユニットテストを 1 件追加し、仕様を固定 75 | 76 | ### 🟢 Green — 最小実装でテストを通す 77 | - 余計なロジックを加えず最小コードでテストを通過 78 | - 必要な箇所には簡潔な日本語コメントで本質的な意図のみを記述 79 | 80 | ### 🟡 Refactor — 品質と構造の改善 81 | - DRY、命名整理、単純化、`black`・`flake8`・`mypy` 準拠 82 | - テストは常に Green を維持 83 | 84 | ### ⚪ Commit — 意味単位で保存 85 | 86 | 1. **todo.md を更新** 87 | - 完了タスクを ✅ にチェック 88 | - タスク全文(箇条書き内容を含む)を `docs/dev-log/` に移動して履歴化 89 | 90 | 2. **実装ログを作成** 91 | - `docs/dev-log/yyyy-mm-dd_hh-mm_機能名.md` を新規作成(※現在時刻を確認) 92 | - 以下を簡潔に記載 93 | - 完了タスク全文 94 | - 実装の背景 95 | - 設計意図 96 | - 副作用 / 注意点 97 | - 関連ファイル・関数 98 | 99 | 3. **Git 操作** 100 | - `git add .` でコード・todo.md・ログをステージング 101 | - `pytest` で最終テスト確認 102 | - 意図が伝わるコミットメッセージで `git commit` 103 | - 変更をリモートへ `git push` 104 | 105 | > こうすることで **コード・タスク管理・実装ログ** が同一コミットに揃い、 106 | > 履歴と進捗が完全に同期される。 107 | 108 | --- 109 | 110 | ## Phase 4 VERIFY(統合検証・記録・コミット) 111 | 112 | **目的** 113 | - 要件・品質基準への最終適合と成果物固定 114 | 115 | **行動** 116 | 1. すべてのタスクについて統合テスト (`pytest` 全体) を実行 117 | 2. 全コードに `black --check`, `flake8`, `mypy` を適用し合格を確認 118 | 3. 合格したタスクを `todo.md` で ✅ に更新し、該当タスク全文を 119 | `docs/dev-log/yyyy-mm-dd_hh-mm_機能名.md` に移動して以下を記録: 120 | - タスク全文 121 | - 実装の背景 122 | - 設計意図 123 | - 副作用 / 注意点 124 | - 関連ファイル・関数 125 | 4. 変更一式(コード・todo.md・ログ)を `git add .` でステージングし、 126 | 意図が伝わるコミットメッセージで `git commit && git push` 127 | 5. **意味ある単位として完結**し、次フェーズへ進める状態になっている 128 | 129 | --- 130 | -------------------------------------------------------------------------------- /docs/architecture.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sito-sikino/avatar-ui-core/332765ab57b9942b80c8f1f0395b648047963514/docs/architecture.md -------------------------------------------------------------------------------- /docs/assets/avatar-ui_demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sito-sikino/avatar-ui-core/332765ab57b9942b80c8f1f0395b648047963514/docs/assets/avatar-ui_demo.gif -------------------------------------------------------------------------------- /docs/spec.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sito-sikino/avatar-ui-core/332765ab57b9942b80c8f1f0395b648047963514/docs/spec.md -------------------------------------------------------------------------------- /docs/todo.md: -------------------------------------------------------------------------------- 1 | # Phase 2 PLAN — todo.md(マイクロタスク+受け入れ条件) 2 | 3 | > 準拠: spec.md / architecture.md / CLAUDE.md 4 | 5 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | flask==3.0.0 2 | google-generativeai==0.8.3 3 | python-dotenv==1.0.0 -------------------------------------------------------------------------------- /settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | 設定管理モジュール - .envファイルから全設定を読み込み 3 | """ 4 | import os 5 | from dotenv import load_dotenv 6 | 7 | # .envファイルを読み込み 8 | load_dotenv() 9 | 10 | # =========================================== 11 | # 必須設定(環境変数が設定されていない場合はエラー) 12 | # =========================================== 13 | 14 | # AI設定 15 | GEMINI_API_KEY = os.environ['GEMINI_API_KEY'] # Gemini APIキー 16 | MODEL_NAME = os.environ['MODEL_NAME'] # 使用するGeminiモデル 17 | 18 | # =========================================== 19 | # 任意設定(デフォルト値あり) 20 | # =========================================== 21 | 22 | # アバター設定 23 | AVATAR_NAME = os.getenv('AVATAR_NAME', 'Spectra') 24 | AVATAR_FULL_NAME = os.getenv('AVATAR_FULL_NAME', 'Spectra Communicator') 25 | AVATAR_IMAGE_IDLE = os.getenv('AVATAR_IMAGE_IDLE', 'idle.png') 26 | AVATAR_IMAGE_TALK = os.getenv('AVATAR_IMAGE_TALK', 'talk.png') 27 | 28 | # AI性格設定(AVATAR_NAMEに依存) 29 | SYSTEM_INSTRUCTION = os.getenv( 30 | 'SYSTEM_INSTRUCTION', 31 | f'あなたは{AVATAR_NAME}というAIアシスタントです。技術的で直接的なスタイルで簡潔に応答してください。回答は短く要点を押さえたものにしてください。' 32 | ) 33 | 34 | # サーバー設定 35 | SERVER_PORT = int(os.getenv('SERVER_PORT', '5000')) 36 | DEBUG_MODE = os.getenv('DEBUG_MODE', 'True').lower() == 'true' 37 | 38 | # UI設定 39 | TYPEWRITER_DELAY_MS = int(os.getenv('TYPEWRITER_DELAY_MS', '50')) 40 | MOUTH_ANIMATION_INTERVAL_MS = int(os.getenv('MOUTH_ANIMATION_INTERVAL_MS', '150')) 41 | 42 | # サウンド設定 43 | BEEP_FREQUENCY_HZ = int(os.getenv('BEEP_FREQUENCY_HZ', '800')) 44 | BEEP_DURATION_MS = int(os.getenv('BEEP_DURATION_MS', '50')) 45 | BEEP_VOLUME = float(os.getenv('BEEP_VOLUME', '0.05')) 46 | BEEP_VOLUME_END = float(os.getenv('BEEP_VOLUME_END', '0.01')) -------------------------------------------------------------------------------- /static/css/style.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500&display=swap'); 2 | 3 | * { 4 | margin: 0; 5 | padding: 0; 6 | box-sizing: border-box; 7 | } 8 | 9 | body { 10 | background: #000; 11 | color: #0f0; 12 | font-family: 'Fira Code', monospace; 13 | font-size: 14px; 14 | height: 100vh; 15 | overflow: hidden; 16 | } 17 | 18 | .terminal { 19 | height: 100vh; 20 | display: flex; 21 | flex-direction: column; 22 | max-width: 1200px; 23 | margin: 0 auto; 24 | padding: 20px; 25 | } 26 | 27 | 28 | .chat-area { 29 | width: 100%; 30 | max-width: 1160px; 31 | height: 300px; 32 | display: flex; 33 | flex-direction: column; 34 | border-top: 2px solid #0f0; 35 | padding-top: 15px; 36 | } 37 | 38 | .chat-container { 39 | width: 100%; 40 | flex: 1; 41 | margin-bottom: 10px; 42 | display: flex; 43 | gap: 10px; 44 | min-height: 0; 45 | } 46 | 47 | .avatar-area { 48 | width: 160px; 49 | border: 1px solid #0f0; 50 | background: rgba(0, 20, 0, 0.1); 51 | display: flex; 52 | flex-direction: column; 53 | align-items: center; 54 | justify-content: center; 55 | padding: 15px; 56 | } 57 | 58 | #avatar-img { 59 | width: 140px; 60 | height: 140px; 61 | image-rendering: pixelated; 62 | margin-bottom: 10px; 63 | } 64 | 65 | .avatar-label { 66 | color: #0f0; 67 | font-size: 12px; 68 | text-align: center; 69 | } 70 | 71 | 72 | 73 | .output { 74 | flex: 1; 75 | overflow-y: auto; 76 | padding: 15px; 77 | border: 1px solid #0f0; 78 | background: rgba(0, 20, 0, 0.1); 79 | height: 100%; 80 | max-height: 100%; 81 | } 82 | 83 | 84 | .line { 85 | margin: 8px 0; 86 | padding: 4px; 87 | word-wrap: break-word; 88 | } 89 | 90 | 91 | 92 | .line.system { 93 | color: #0f0; 94 | opacity: 0.7; 95 | } 96 | 97 | .line.user { 98 | color: #0ff; 99 | border-left: 2px solid #0ff; 100 | padding-left: 10px; 101 | } 102 | 103 | .line.ai { 104 | color: #0f0; 105 | border-left: 2px solid #0f0; 106 | padding-left: 10px; 107 | } 108 | 109 | .line.error { 110 | color: #f00; 111 | border-left: 2px solid #f00; 112 | padding-left: 10px; 113 | } 114 | 115 | .user-prompt { 116 | color: #0ff; 117 | font-weight: 500; 118 | } 119 | 120 | .ai-prompt { 121 | color: #0f0; 122 | font-weight: 500; 123 | } 124 | 125 | .error-prompt { 126 | color: #f00; 127 | font-weight: 500; 128 | } 129 | 130 | .input-line { 131 | width: 100%; 132 | display: flex; 133 | align-items: center; 134 | padding: 10px; 135 | border: 1px solid #0f0; 136 | background: rgba(0, 20, 0, 0.1); 137 | } 138 | 139 | .prompt { 140 | color: #0f0; 141 | margin-right: 10px; 142 | } 143 | 144 | #input { 145 | flex: 1; 146 | background: transparent; 147 | border: none; 148 | color: #0f0; 149 | font-family: inherit; 150 | font-size: inherit; 151 | outline: none; 152 | } 153 | 154 | 155 | -------------------------------------------------------------------------------- /static/images/idle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sito-sikino/avatar-ui-core/332765ab57b9942b80c8f1f0395b648047963514/static/images/idle.png -------------------------------------------------------------------------------- /static/images/talk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sito-sikino/avatar-ui-core/332765ab57b9942b80c8f1f0395b648047963514/static/images/talk.png -------------------------------------------------------------------------------- /static/js/animation.js: -------------------------------------------------------------------------------- 1 | /** 2 | * アニメーション管理モジュール 3 | * タイプライター効果と口パクアニメーションを管理 4 | */ 5 | export class AnimationManager { 6 | constructor(settings, soundManager) { 7 | this.settings = settings; 8 | this.soundManager = soundManager; 9 | this.talkingInterval = null; 10 | this.avatarImg = document.getElementById('avatar-img'); 11 | this.output = document.getElementById('output'); 12 | } 13 | 14 | // 口パクアニメーション開始 15 | startMouthAnimation() { 16 | if (this.talkingInterval) { 17 | this.stopMouthAnimation(); 18 | } 19 | 20 | let mouthOpen = false; 21 | this.talkingInterval = setInterval(() => { 22 | const imagePath = this.settings.getAvatarImagePath(!mouthOpen); 23 | this.avatarImg.src = imagePath; 24 | mouthOpen = !mouthOpen; 25 | }, this.settings.mouthAnimationInterval); 26 | } 27 | 28 | // 口パクアニメーション停止 29 | stopMouthAnimation() { 30 | if (this.talkingInterval) { 31 | clearInterval(this.talkingInterval); 32 | this.talkingInterval = null; 33 | } 34 | // アイドル状態に戻す 35 | this.avatarImg.src = this.settings.getAvatarImagePath(true); 36 | } 37 | 38 | // タイプライター効果 39 | typeWriter(element, text) { 40 | return new Promise((resolve) => { 41 | let i = 0; 42 | 43 | // 口パクアニメーション開始 44 | this.startMouthAnimation(); 45 | 46 | const type = () => { 47 | if (i < text.length) { 48 | element.textContent += text.charAt(i++); 49 | this.output.scrollTop = this.output.scrollHeight; 50 | 51 | // スペース以外で音を鳴らす 52 | if (text.charAt(i-1) !== ' ') { 53 | this.soundManager.playTypeSound(); 54 | } 55 | 56 | setTimeout(type, this.settings.typewriterDelay); 57 | } else { 58 | // 完了時:口を閉じる 59 | this.stopMouthAnimation(); 60 | resolve(); 61 | } 62 | }; 63 | 64 | type(); 65 | }); 66 | } 67 | } -------------------------------------------------------------------------------- /static/js/app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * メインアプリケーションエントリーポイント 3 | * 各モジュールを初期化し、アプリケーションを起動 4 | */ 5 | import { createSettings } from './settings.js'; 6 | import { SoundManager } from './sound.js'; 7 | import { AnimationManager } from './animation.js'; 8 | import { ChatManager } from './chat.js'; 9 | 10 | // DOMロード完了後にアプリケーション初期化 11 | document.addEventListener('DOMContentLoaded', () => { 12 | // グローバル変数appConfigはindex.htmlで定義される 13 | if (typeof appConfig !== 'undefined') { 14 | const settings = createSettings(appConfig); 15 | const soundManager = new SoundManager(settings); 16 | const animationManager = new AnimationManager(settings, soundManager); 17 | window.chatManager = new ChatManager(settings, animationManager); 18 | } else { 19 | console.error('App config not found'); 20 | } 21 | }); -------------------------------------------------------------------------------- /static/js/chat.js: -------------------------------------------------------------------------------- 1 | /** 2 | * チャット機能モジュール 3 | * メッセージの送受信とUI更新を管理 4 | */ 5 | export class ChatManager { 6 | constructor(settings, animationManager) { 7 | this.settings = settings; 8 | this.animationManager = animationManager; 9 | this.output = document.getElementById('output'); 10 | this.input = document.getElementById('input'); 11 | 12 | this.initEventListeners(); 13 | } 14 | 15 | // イベントリスナー初期化 16 | initEventListeners() { 17 | this.input.addEventListener('keypress', async (e) => { 18 | if (e.key === 'Enter' && this.input.value.trim()) { 19 | await this.sendMessage(this.input.value); 20 | this.input.value = ''; 21 | } 22 | }); 23 | } 24 | 25 | // メッセージ送信 26 | async sendMessage(message) { 27 | // ユーザーメッセージを表示 28 | this.addLine(message, 'user'); 29 | 30 | try { 31 | // AIに送信 32 | const response = await fetch('/api/chat', { 33 | method: 'POST', 34 | headers: {'Content-Type': 'application/json'}, 35 | body: JSON.stringify({message}) 36 | }); 37 | 38 | const data = await response.json(); 39 | 40 | // AIレスポンスをタイプライター効果で表示 41 | await this.addLine(data.response, 'ai'); 42 | } catch (error) { 43 | console.error('Chat error:', error); 44 | this.addLine('エラーが発生しました。再試行してください。', 'system'); 45 | } 46 | } 47 | 48 | // メッセージを画面に追加 49 | async addLine(text, type) { 50 | const line = document.createElement('div'); 51 | line.className = 'line ' + type; 52 | 53 | if (type === 'user') { 54 | line.innerHTML = `USER> ${text}`; 55 | this.output.appendChild(line); 56 | this.scrollToBottom(); 57 | } else if (type === 'ai') { 58 | // AIメッセージはタイプライター演出 59 | line.innerHTML = `${this.settings.avatarName}> `; 60 | this.output.appendChild(line); 61 | 62 | const aiTextElement = line.querySelector('.ai-text'); 63 | await this.animationManager.typeWriter(aiTextElement, text); 64 | } else { 65 | // system メッセージなど 66 | line.textContent = text; 67 | this.output.appendChild(line); 68 | this.scrollToBottom(); 69 | } 70 | } 71 | 72 | // チャットエリアを最下部にスクロール 73 | scrollToBottom() { 74 | this.output.scrollTop = this.output.scrollHeight; 75 | } 76 | } -------------------------------------------------------------------------------- /static/js/settings.js: -------------------------------------------------------------------------------- 1 | /** 2 | * アプリケーション設定モジュール 3 | * サーバーサイドから渡される設定値を管理 4 | */ 5 | export const createSettings = (config) => ({ 6 | ...config, 7 | // アバター画像のパスを生成 8 | getAvatarImagePath: (isIdle = true) => 9 | `/static/images/${isIdle ? config.avatarImageIdle : config.avatarImageTalk}` 10 | }); -------------------------------------------------------------------------------- /static/js/sound.js: -------------------------------------------------------------------------------- 1 | /** 2 | * サウンド生成・再生モジュール 3 | * タイプライター効果音の生成と再生を管理 4 | */ 5 | export class SoundManager { 6 | constructor(settings) { 7 | this.settings = settings; 8 | this.audioContext = null; 9 | this.initAudioContext(); 10 | } 11 | 12 | // Web Audio Context初期化 13 | initAudioContext() { 14 | try { 15 | this.audioContext = new (window.AudioContext || window.webkitAudioContext)(); 16 | } catch (error) { 17 | // Audio未対応環境では無音で続行 18 | } 19 | } 20 | 21 | // タイプライター効果音を再生 22 | playTypeSound() { 23 | if (!this.audioContext) return; 24 | 25 | try { 26 | const oscillator = this.audioContext.createOscillator(); 27 | const gainNode = this.audioContext.createGain(); 28 | 29 | oscillator.connect(gainNode); 30 | gainNode.connect(this.audioContext.destination); 31 | 32 | oscillator.type = 'square'; // 矩形波 33 | oscillator.frequency.setValueAtTime( 34 | this.settings.beepFrequency, 35 | this.audioContext.currentTime 36 | ); 37 | 38 | gainNode.gain.setValueAtTime( 39 | this.settings.beepVolume, 40 | this.audioContext.currentTime 41 | ); 42 | const durationInSeconds = this.settings.beepDuration / 1000; // ms→秒変換 43 | gainNode.gain.exponentialRampToValueAtTime( 44 | this.settings.beepVolumeEnd, 45 | this.audioContext.currentTime + durationInSeconds 46 | ); 47 | 48 | oscillator.start(this.audioContext.currentTime); 49 | oscillator.stop(this.audioContext.currentTime + durationInSeconds); 50 | } catch (e) { 51 | // 音声再生エラーは無視 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{ config.avatar_full_name }} 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 | 16 |
17 |
> SYSTEM: {{ config.avatar_full_name }} Online
18 |
19 | 20 | 21 |
22 | {{ config.avatar_name|upper }} 23 |
{{ config.avatar_name|upper }}
24 |
25 |
26 | 27 | 28 |
29 | > 30 | 31 |
32 |
33 |
34 | 35 | 49 | 50 | 51 | 52 | 53 | --------------------------------------------------------------------------------