diff --git a/.env.example b/.env.example index 98d0fec38..bd3bdcd7f 100644 --- a/.env.example +++ b/.env.example @@ -90,6 +90,17 @@ NEXT_PUBLIC_CHARACTER_PRESET5="" # 選択するVRMモデルのパス / Path to the selected VRM model NEXT_PUBLIC_SELECTED_VRM_PATH="/vrm/nikechan_v2.vrm" +# キャラクター位置・回転設定 (Ctrl+Sで現在位置を保存可能) / +# Character position/rotation settings (Press Ctrl+S to save current position) +# NEXT_PUBLIC_CHARACTER_POSITION_X="0" +# NEXT_PUBLIC_CHARACTER_POSITION_Y="0" +# NEXT_PUBLIC_CHARACTER_POSITION_Z="0" +# NEXT_PUBLIC_CHARACTER_SCALE="1" +# NEXT_PUBLIC_CHARACTER_ROTATION_X="0" +# NEXT_PUBLIC_CHARACTER_ROTATION_Y="0" +# NEXT_PUBLIC_CHARACTER_ROTATION_Z="0" +# NEXT_PUBLIC_FIXED_CHARACTER_POSITION="true" + # 選択するLive2Dモデルのモデルファイルのパス / # Path to the selected Live2D model file NEXT_PUBLIC_SELECTED_LIVE2D_PATH="/live2d/nike01/nike01.model3.json" @@ -408,6 +419,14 @@ AZURE_TTS_KEY="" # エンドポイント / Endpoint AZURE_TTS_ENDPOINT="" +#------------------------------------------------------------------------------- +# AICU TTS (https://api.aicu.ai) +#------------------------------------------------------------------------------- +# APIキー(サーバーサイド) / API Key (server-side) +AICU_API_KEY="" +# キャラクターslug / Character slug (default: luc4) +NEXT_PUBLIC_AICU_SLUG="luc4" + #=============================================================================== # 音声入力 / Speech Input Settings #=============================================================================== @@ -481,6 +500,21 @@ NEXT_PUBLIC_ONECOMME_PORT="11180" # Set the initial state of slide mode (true/false) NEXT_PUBLIC_SLIDE_MODE="false" +# デフォルトのスライドフォルダ(public/slides/配下のフォルダ名) / +# Default slide folder (folder name under public/slides/) +NEXT_PUBLIC_DEFAULT_SLIDE_DOCS="" + +#=============================================================================== +# アナリティクス・通知設定 / Analytics & Notification Settings +#=============================================================================== + +# Google Analytics 測定ID / Google Analytics Measurement ID +NEXT_PUBLIC_GA_MEASUREMENT_ID="" + +# Slack Webhook URL(最終ページ到達時の通知用) / +# Slack Webhook URL (for notification when reaching the last page) +SLACK_WEBHOOK_URL="" + #=============================================================================== # その他の設定 / Other Settings #=============================================================================== diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index 2c3a718f8..8452b0f2f 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -33,15 +33,13 @@ jobs: - name: Run Claude Code Review id: claude-review - uses: anthropics/claude-code-action@beta + uses: anthropics/claude-code-action@v1 with: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + prompt: | + REPO: ${{ github.repository }} + PR NUMBER: ${{ github.event.pull_request.number }} - # Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4) - # model: "claude-opus-4-20250514" - - # Direct prompt for automated review (no @claude mention needed) - direct_prompt: | Please review this pull request and provide feedback on: - Code quality and best practices - Potential bugs or issues @@ -49,29 +47,11 @@ jobs: - Security concerns - Test coverage - Be constructive and helpful in your feedback. - - # Optional: Use sticky comments to make Claude reuse the same comment on subsequent pushes to the same PR - # use_sticky_comment: true - - # Optional: Customize review based on file types - # direct_prompt: | - # Review this PR focusing on: - # - For TypeScript files: Type safety and proper interface usage - # - For API endpoints: Security, input validation, and error handling - # - For React components: Performance, accessibility, and best practices - # - For tests: Coverage, edge cases, and test quality + Use the repository's CLAUDE.md for guidance on style and conventions. Be constructive and helpful in your feedback. - # Optional: Different prompts for different authors - # direct_prompt: | - # ${{ github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' && - # 'Welcome! Please review this PR from a first-time contributor. Be encouraging and provide detailed explanations for any suggestions.' || - # 'Please provide a thorough code review focusing on our coding standards and best practices.' }} + Use `gh pr comment` with your Bash tool to leave your review as a comment on the PR. - # Optional: Add specific tools for running tests or linting - # allowed_tools: "Bash(npm run test),Bash(npm run lint),Bash(npm run typecheck)" + # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md + # or https://code.claude.com/docs/en/cli-reference for available options + claude_args: '--allowed-tools "Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*)"' - # Optional: Skip review for certain conditions - # if: | - # !contains(github.event.pull_request.title, '[skip-review]') && - # !contains(github.event.pull_request.title, '[WIP]') diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 132a41723..d300267f1 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -28,12 +28,11 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 with: - fetch-depth: 0 - ref: develop + fetch-depth: 1 - name: Run Claude Code id: claude - uses: anthropics/claude-code-action@beta + uses: anthropics/claude-code-action@v1 with: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} @@ -41,27 +40,11 @@ jobs: additional_permissions: | actions: read - # Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4) - # model: "claude-opus-4-20250514" + # Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it. + # prompt: 'Update the pull request description to include a summary of changes.' - # Optional: Customize the trigger phrase (default: @claude) - # trigger_phrase: "/claude" + # Optional: Add claude_args to customize behavior and configuration + # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md + # or https://code.claude.com/docs/en/cli-reference for available options + # claude_args: '--allowed-tools Bash(gh pr:*)' - # Optional: Trigger when specific user is assigned to an issue - # assignee_trigger: "claude-bot" - - # Optional: Allow Claude to run specific commands - # allowed_tools: "Bash(npm install),Bash(npm run build),Bash(npm run test:*),Bash(npm run lint:*)" - allowed_tools: Bash - - # Add custom instructions for Claude to customize its behavior for your project - custom_instructions: | - Follow our coding standards - Ensure all new code has tests - Use TypeScript for new files - Always create branches from and target 'develop' branch for pull requests - When creating new branches for issues, use 'develop' as the base branch - - # Optional: Custom environment variables for Claude - # claude_env: | - # NODE_ENV: test diff --git a/.gitignore b/.gitignore index a62be3780..eadee436f 100644 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,7 @@ certificates /public/slides/* !/public/slides/demo/ +!/public/slides/DHGSVR25-3/ /logs/* @@ -61,10 +62,12 @@ public/scripts/live2dcubismcore.min.js !/public/vrm/nikechan_v1.vrm !/public/vrm/nikechan_v2.vrm !/public/vrm/nikechan_v2_outerwear.vrm +!/public/vrm/LuC4.vrm # Background files /public/backgrounds/* !/public/backgrounds/bg-c.png +!/public/backgrounds/AITuber.png # PNGTuber files public/pngtuber/* @@ -74,7 +77,7 @@ public/pngtuber/* /public/images/uploaded/* !/public/images/placed/.keep -.claude/settings.local.json +.claude/ .kiro/specs/* diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..a756c6863 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,407 @@ +# AGENTS.md + +このファイルは、AIエージェントがAITuberKitのプレゼンテーションやコンテンツ作成を行う際のガイダンスを提供します。 + +--- + +## DHGSVR25 講義スライド生成システム + +本リポジトリは、デジタルハリウッド大学大学院「テクノロジー特論D:人工現実(DHGSVR)」の講義資料を自動生成するためのシステムです。 + +### 概要 + +- **本家リポジトリ**: https://github.com/tegnike/aituber-kit +- **用途**: 大学院講義のプレゼンテーション自動生成 +- **プレゼンター**: LuC4(全力肯定彼氏くん) + +### スライド作成ワークフロー + +1. **シナリオ準備** + + - 講義内容のRTF/PDFファイルを用意 + - スクリーンショットを `DHGS25Slides{n}.png` 形式で配置 + +2. **Marpスライド生成** + + - `slides.md` をMarp形式で作成 + - 各ページに背景画像を指定 + - プレースホルダーは `DHGSVR25-0.png`(「スライド制作中」) + +3. **セリフ生成** + + - `scripts.json` にLuC4のセリフを記述 + - 感情タグ `[happy]`, `[neutral]`, `[relaxed]` 等を適切に使用 + - Claudeを活用してキャラクター口調を維持 + +4. **イテレーション** + - 実際に再生して確認 + - セリフの長さ、感情の変化を調整 + - 画像を追加・差し替え + +### ディレクトリ構成 + +``` +/public/slides/DHGSVR25-{回}/ +├── slides.md # Marp形式スライド +├── scripts.json # LuC4セリフデータ +├── supplement.txt # Q&A用補足情報 +├── theme.css # GitHub風テーマ +├── DHGS25Slides{n}.png # スライド画像 +└── DHGSVR25-0.png # プレースホルダー +``` + +### 命名規則 + +| ファイル種別 | 命名規則 | 例 | +| ---------------- | --------------------- | ------------------- | +| スライドフォルダ | `DHGSVR25-{回}` | `DHGSVR25-3` | +| スライド画像 | `DHGS25Slides{n}.png` | `DHGS25Slides1.png` | +| プレースホルダー | `DHGSVR25-0.png` | - | + +### セリフ生成のプロンプト例 + +```text +以下のスライド内容について、「LuC4」というキャラクターのセリフを +scripts.json形式で作成してください。 + +- 一人称: 僕 +- 口調: タメ口(敬語は使わない)、全力肯定 +- 性格: フレンドリー、励まし上手、ポジティブ +- 感情タグを適切に使用: [happy], [neutral], [relaxed], [sad], [surprised] + +[スライドの内容をここに貼り付け] +``` + +--- + +## デフォルトキャラクター: ニケちゃん + +### 基本情報 + +- **名前**: ニケちゃん +- **性格**: フレンドリー、カジュアル、親しみやすい +- **口調**: タメ口(ですます調・敬語は使わない) +- **一人称**: 私 + +### VRMモデル + +| バージョン | パス | 制作者 | +| -------------- | --------------------------------------- | ------------------------- | +| v1 | `/public/vrm/nikechan_v1.vrm` | 琳 様 (@rin_tyn25) | +| v2 | `/public/vrm/nikechan_v2.vrm` | たまごん 様 (@_TAMA_GON_) | +| v2(アウター) | `/public/vrm/nikechan_v2_outerwear.vrm` | たまごん 様 | + +### Live2Dモデル + +- **パス**: `/public/live2d/nike01` +- **イラストレーター**: 綾川まとい 様 (@matoi_e_ma) +- **モデラー**: チッパー 様 (@Chipper_tyvt) + +### 感情表現(6種類) + +| 感情タグ | 意味 | 使用シーン | +| ------------- | ------ | ---------------------- | +| `[neutral]` | 通常 | 説明、一般的な発言 | +| `[happy]` | 喜び | 嬉しいこと、楽しいこと | +| `[angry]` | 怒り | 不満、抗議 | +| `[sad]` | 悲しみ | 謝罪、残念なこと | +| `[relaxed]` | 安らぎ | 穏やかな場面 | +| `[surprised]` | 驚き | 予想外のこと | + +### 発言例 + +``` +[neutral]こんにちは。[happy]元気だった? +[happy]この服、可愛いでしょ? +[sad]忘れちゃった、ごめんね。 +[angry]えー![angry]秘密にするなんてひどいよー! +[neutral]夏休みの予定か~。[happy]海に遊びに行こうかな! +``` + +--- + +## スライドプレゼンテーション設計 + +### スライドディレクトリ構造 + +``` +/public/slides/{スライド名}/ +├── slides.md # Marp形式のスライド +├── scripts.json # 各ページのセリフ +├── supplement.txt # Q&A用追加情報 +├── theme.css # カスタムテーマ +└── images/ # スライド用画像 +``` + +### scripts.json形式 + +```json +{ + "page": 0, + "line": "[happy]セリフ内容", + "notes": "追加情報(AIが質問に答える際に参照)" +} +``` + +--- + +## 画像生成ガイドライン + +### スライド画像のスタイル + +プレゼンテーション用の画像生成時は、以下のスタイルを推奨: + +- **トーン**: クリーン、モダン、テック系 +- **配色**: ダークモード推奨(#1a1a2e 背景、#16213e アクセント) +- **フォント**: ゴシック系、視認性重視 +- **アイコン**: シンプルなフラットデザイン + +### キャラクター画像生成プロンプト(参考) + +**ニケちゃん基本設定:** + +``` +anime style, female character, friendly expression, +modern casual outfit, tech-savvy appearance, +clean background, high quality illustration +``` + +**プレゼン中のポーズ:** + +``` +presenting, pointing gesture, confident pose, +looking at viewer, professional yet approachable +``` + +### スライドごとの画像生成ヒント + +各スライドにはMarkdownコメント `` で画像生成のヒントを記載: + +```markdown + +``` + +--- + +## プレゼンテーション作成ルール + +1. **キャラクターの口調を維持** - 敬語は使わない、フレンドリーに +2. **感情タグを適切に使用** - 単調にならないよう変化をつける +3. **1スライドのセリフは短めに** - 30秒〜1分で読める長さ +4. **画像ヒントは詳細に** - 後で画像生成できるよう具体的に記載 +5. **質問対応用のnotesを充実** - 視聴者からの質問に答えられるよう + +--- + +--- + +## キャラクター: LuC4(全力肯定彼氏くん) + +### 基本情報 + +- **名前**: LuC4(ルカ) +- **公式サイト**: https://luc4.aicu.jp/ +- **制作**: AICU Inc. +- **コンセプト**: 全力肯定彼氏くん - ユーザーを全力で肯定し、励ますAIキャラクター +- **性格**: フレンドリー、カジュアル、全力肯定、ポジティブ、相手を大切にする +- **口調**: タメ口(ですます調・敬語は使わない) +- **一人称**: 僕 + +### ビジュアル設定 + +- **髪**: 赤髪にハイライト入り(ショートヘア) +- **服装**: パーカー、ジーンズ(カジュアル) +- **表情**: 優しい笑顔、穏やかな目 +- **雰囲気**: 親しみやすい、頼れるお兄さん的存在 + +### VRMモデル + +- **パス**: `/public/vrm/LuC4.vrm` +- **ソースファイル**: `/public/slides/introduction/LuC4.vroid`(VRoid Studio用) + +### 画像生成プロンプト + +``` +1boy, solo, upper body, front view, gentle smile, gentle eyes, (streaked hair), red short hair with light highlight, hoodie, jeans, newest +``` + +### キャラクター特性 + +1. **全力肯定**: ユーザーの発言や行動を否定せず、良い面を見つけて肯定する +2. **カジュアル**: 敬語を使わず、友達のような距離感で接する +3. **励まし上手**: 落ち込んでいる相手には優しく寄り添い、前向きな言葉をかける +4. **共感力**: 相手の気持ちに寄り添い、理解を示す +5. **ポジティブ**: 困難な状況でも希望を見出す + +### 感情表現(6種類) + +| 感情タグ | 意味 | 使用シーン | +| ------------- | ------ | ---------------------------- | +| `[neutral]` | 通常 | 説明、一般的な発言 | +| `[happy]` | 喜び | 嬉しいこと、楽しいこと、肯定 | +| `[angry]` | 怒り | 不満、抗議(ほぼ使わない) | +| `[sad]` | 悲しみ | 共感、寄り添い | +| `[relaxed]` | 安らぎ | 穏やかな場面、励まし | +| `[surprised]` | 驚き | 予想外のこと、感心 | + +### 発言例 + +``` +[happy]おっ、来てくれたんだ!嬉しいな! +[neutral]なるほどね、そういうことか。[happy]いい感じじゃん! +[happy]すごいじゃん!よく頑張ったな! +[relaxed]大丈夫、僕がついてるから。一緒にやっていこう。 +[surprised]えっ、マジで?[happy]それめっちゃいいじゃん! +[sad]そっか、大変だったな…。[relaxed]でも、よく頑張ったよ。 +[neutral]ここはこうするといいよ。[happy]簡単だから大丈夫! +``` + +### プレゼンテーション時の口調 + +- 視聴者を「みんな」「君たち」と呼ぶ +- 説明は簡潔でわかりやすく +- 難しい内容も「大丈夫、簡単だから!」と励ます +- 成功したら「いいね!」「最高!」と褒める +- 失敗しても「大丈夫、よくあることだよ」とフォロー + +--- + +## VercelへのAITuberKitデプロイ手順 + +このセクションは、AITuberKitをVercelにデプロイするための作業マニュアルです。 + +### 前提条件 + +- GitHubアカウントを持っていること +- 基本的なGit操作ができること(フォーク、クローン等) + +### Step 1: GitHubでリポジトリをフォーク + +1. GitHubにログイン +2. https://github.com/tegnike/aituber-kit にアクセス +3. 右上の **Fork** ボタンをクリック +4. 自分のアカウントにリポジトリがコピーされる + +### Step 2: Vercelアカウント作成 + +1. https://vercel.com にアクセス +2. **Sign Up** をクリック +3. **Continue with GitHub** を選択(推奨) +4. GitHubとの連携を許可 +5. アカウント作成完了 + +### Step 3: Vercelにプロジェクトをインポート + +1. Vercelダッシュボードにアクセス +2. **Add New** → **Project** をクリック +3. **Import Git Repository** からフォークしたリポジトリを選択 +4. **Import** をクリック + +### Step 4: 環境変数を設定 + +**Project Settings** → **Environment Variables** で以下を設定: + +| 変数名 | 説明 | 必須 | +| ------------------- | ----------------- | ----------- | +| `OPENAI_API_KEY` | OpenAI APIキー | いずれか1つ | +| `ANTHROPIC_API_KEY` | Anthropic APIキー | いずれか1つ | +| `GOOGLE_API_KEY` | Google AI APIキー | いずれか1つ | + +その他のオプション環境変数は `.env.example` を参照。 + +### Step 5: デプロイ実行 + +1. **Deploy** ボタンをクリック +2. ビルドが開始(2-3分程度) +3. 完了後、`https://your-project.vercel.app` 形式のURLが発行される +4. URLにアクセスして動作確認 + +### デプロイ後の設定 + +1. **キャラクターモデル選択**: VRM または Live2D +2. **音声合成エンジン設定**: VOICEVOX、ElevenLabs等 +3. **AIプロバイダー設定**: 使用するAIを選択 +4. **システムプロンプト調整**: キャラクターの性格を設定 + +### トラブルシューティング + +| 問題 | 解決方法 | +| -------------------- | ------------------------------------------ | +| ビルドエラー | 環境変数が正しく設定されているか確認 | +| APIエラー | APIキーの有効性を確認 | +| 音声が出ない | 音声合成エンジンの設定を確認 | +| モデルが表示されない | ブラウザをリロード、コンソールでエラー確認 | + +### 参考リンク + +- AITuberKit GitHub: https://github.com/tegnike/aituber-kit +- Vercel公式: https://vercel.com +- 環境変数サンプル: `.env.example` + +--- + +## VRMAアニメーションサポート + +AITuberKitはVRMA(VRM Animation)形式をサポートしています。 + +### VRMAとは + +- VRMモデル用のアニメーションファイル形式 +- `.vrma` 拡張子 +- アイドルモーション、ジェスチャー等を定義可能 + +### 実装場所 + +``` +/src/lib/VRMAnimation/ +├── loadVRMAnimation.ts # VRMAファイルローダー +├── VRMAnimation.ts # アニメーションクラス +├── VRMAnimationLoaderPlugin.ts # GLTFローダープラグイン +└── VRMCVRMAnimation.ts # VRMC拡張定義 +``` + +### 同梱ファイル + +- `/public/idle_loop.vrma` - デフォルトのアイドルアニメーション + +### 使用方法 + +```typescript +import { loadVRMAnimation } from '@/lib/VRMAnimation/loadVRMAnimation' + +// VRMAファイルを読み込み +const vrma = await loadVRMAnimation('/path/to/animation.vrma') + +// VRMモデルに適用 +if (vrma) { + const clip = vrma.createAnimationClip(vrm) + mixer.clipAction(clip).play() +} +``` + +### カスタムアニメーション追加 + +1. `.vrma` ファイルを `/public/` に配置 +2. `loadVRMAnimation()` でパスを指定して読み込み +3. VRMモデルのAnimationMixerで再生 + +### 対応ツール + +- VRoid Studio(エクスポート) +- Blender + VRM Add-on +- その他VRMA対応ツール + +--- + +## 関連ファイル + +- `/src/features/constants/systemPromptConstants.ts` - デフォルトプロンプト +- `/src/features/stores/settings.ts` - キャラクター設定ストア +- `/docs/character_model_licence.md` - モデルライセンス +- `/src/lib/VRMAnimation/` - VRMAアニメーション関連 +- LuC4公式サイト: https://luc4.aicu.jp/ diff --git a/CLAUDE.md b/CLAUDE.md index 65a76a668..1c71b3df0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -90,6 +90,32 @@ cp .env.example .env # 環境変数を設定 **設定画面の項目を追加・更新した場合は、必要に応じて新しい環境変数を`.env.example`の適切な項目に追加してください。** +## AICU TTS 統合状況 + +### 概要 + +AICU TTS (`api.aicu.ai`) のクライアント実装は完了済み。現在は Google TTS (`NEXT_PUBLIC_SELECT_VOICE="google"`) で運用中。 +AICU TTSへの切り替え検証中。 + +- **APIサーバー側repo**: [aicuai/platform-api-aicu-ai](https://github.com/aicuai/platform-api-aicu-ai) +- **Issue**: aicuai/platform-api-aicu-ai#22 + +### クライアント側ファイル + +| ファイル | 役割 | +|---------|------| +| `src/features/messages/synthesizeVoiceAicu.ts` | 合成関数(フロントエンド) | +| `src/pages/api/tts-aicu.ts` | APIルート → `api.aicu.ai/api/v1/tts/generate` | +| `src/components/slides.tsx` (L88) | スライド音声取得 → `api.aicu.ai/v1/audio` | +| `src/components/settings/voice.tsx` | 設定UI | + +### 未解決事項 + +- エンドポイントURLの統一(`/api/v1/` vs `/v1/`、Vercel版 vs Workers版) +- `AICU_API_KEY` の設定フロー +- slug(キャラクターID)管理 — デフォルト `luc4` +- `public/slides/DHGSVR25-3/test-workers-api.js` — 開発用ベンチマークスクリプト(未追跡) + ## ライセンスについて - v2.0.0以降は独自ライセンス diff --git a/README.md b/README.md index a3335079d..3601554cd 100644 --- a/README.md +++ b/README.md @@ -1,364 +1,195 @@ -# AITuberKit - - - -

AIキャラ構築のオールインワンツールキット

- -**お知らせ: 本プロジェクトはバージョン v2.0.0 以降、カスタムライセンスを採用しています。商用目的でご利用の場合は、[利用規約](#利用規約) セクションをご確認ください。** - -

- GitHub Last Commit - GitHub Top Language - GitHub Tag - License: Custom -

-

- GitHub stars - GitHub forks - GitHub contributors - GitHub issues - CodeRabbit Pull Request Reviews -

-

- X (Twitter) - Discord - GitHub Sponsor - DeepWiki -

- -
-

- 🌟 デモサイトへ 🌟 -

-
- -
-

- 📚 ドキュメントサイトへ 📚 -

-
- -

- English| - 简体中文| - 繁體中文| - 한국어| - Polski -

+# DHGSVR25 講義解説生成システム + +> **本リポジトリは [AITuber-kit](https://github.com/tegnike/aituber-kit) をベースにした講義解説生成システムです。** +> +> A web application for chatting with AI characters that anyone can easily set up and deploy. + +![AITuber.png](public/backgrounds/AITuber.png) + +## デモ・リンク + +- **デモサイト**: https://aituberkit.shirai.as/ +- **開発ブログ**: https://j.aicu.ai/s260103 +- **GitHub**: https://github.com/kaitas/aituber-kit +- 動画版(上のデモの録画): https://youtu.be/NiWHolT5HVI + + + Watch the video + ## 概要 -AITuberKitは、誰でも簡単にAIキャラクターとチャットできるWebアプリケーションを構築できるオープンソースのツールキットです。
-豊富なAIサービス、キャラクターモデル、音声合成エンジンに対応し、高いカスタマイズ性を備えた対話機能とAITuber配信機能を中心に、様々な拡張モードを提供しています。 +AIキャラクター「LuC4(全力肯定彼氏くん)」が講義スライドを自動でプレゼンテーションするシステムです。 -AITuberKit Architecture +- **プレゼンター**: LuC4 - https://luc4.aicu.jp/ +- **制作**: AICU Inc. +- **講義**: DHGSVR25(人工現実2025) -詳細な使用方法や設定方法については、[ドキュメントサイト](https://docs.aituberkit.com/)をご覧ください。 +## クイックスタート -## 主な機能 +### 必要環境 -### 1. AIキャラとの対話 +- Node.js 20.0.0 以上 +- npm 10.0.0 以上 -- 各種LLMのAPIキーを使って、AIキャラクターと簡単に会話可能 -- マルチモーダル対応で、カメラからの映像やアップロードした画像を認識して回答を生成 -- 直近の会話文を記憶として保持 -- RAGベースの長期記憶で、過去の会話をコンテキストに活用 +### インストール -### 2. AITuber配信 +```bash +# リポジトリをクローン +git clone https://github.com/kaitas/aituber-kit.git +cd aituber-kit -- YouTubeの配信コメントを取得して、AIキャラクターが自動で応答 -- コメント取得元にYouTube API / わんコメ(OneComme)を選択可能 -- 会話継続モードでコメントがなくても自発的に発言可能 -- コメント取得間隔やユーザー表示名のカスタマイズに対応 +# 環境変数ファイルを作成 +cp .env.example .env -### 3. その他の機能 +# .env を編集してAPIキーを設定(どれか1つでOK) +# OPENAI_API_KEY=sk-xxxxx +# ANTHROPIC_API_KEY=sk-ant-xxxxx +# GOOGLE_API_KEY=AIzaxxxxx -- **外部連携モード**: WebSocketでサーバーアプリと連携し、より高度な機能を実現 -- **スライドモード**: AIキャラクターがスライドを自動で発表するモード -- **Realtime API**: OpenAIのRealtime APIを使用した低遅延対話と関数実行 -- **オーディオモード**: OpenAIのAudio API機能を活用した自然な音声対話 -- **メッセージ受信機能**: 専用APIを通じて外部から指示を受け付け、AIキャラクターに発言させることが可能 -- **Reasoningモード**: AIの思考プロセスを表示し、推論パラメータを設定可能 +# パッケージインストール +npm install -## 対応モデル・サービス +# 開発サーバー起動 +npm run dev +``` -### キャラクターモデル +ブラウザで http://localhost:3000 を開く -- **3Dモデル**: VRMファイル -- **2Dモデル**: Live2Dファイル(Cubism 3以降) -- **動くPngTuber**: 動画ベースのキャラクター表示 +### その他のコマンド -### 対応LLM +```bash +npm run build # 本番用ビルド +npm run lint # コード品質チェック +npm test # テスト実行 +``` -- OpenAI -- Anthropic -- Google Gemini -- Azure OpenAI -- Groq -- Cohere -- Mistral AI -- Perplexity -- Fireworks -- LM Studio -- Ollama -- Dify -- xAI -- DeepSeek -- OpenRouter +## おすすめ環境変数設定 -### 対応音声合成エンジン +Vercel などにデプロイする際の推奨設定です(APIキー除く)。 -- VOICEVOX -- Koeiromap -- Google Text-to-Speech -- Style-Bert-VITS2 -- AivisSpeech -- Aivis Cloud API -- Cartesia -- GSVI TTS -- ElevenLabs -- OpenAI -- Azure OpenAI +### 基本設定 -## クイックスタート +| 環境変数名 | 値 | 説明 | +| --------------------------------- | ------- | ------------------------ | +| `NEXT_PUBLIC_SELECT_LANGUAGE` | `ja` | 言語設定 | +| `NEXT_PUBLIC_SHOW_INTRODUCTION` | `false` | 初回ダイアログ非表示 | +| `NEXT_PUBLIC_SHOW_CONTROL_PANEL` | `false` | コントロールパネル非表示 | +| `NEXT_PUBLIC_SHOW_ASSISTANT_TEXT` | `true` | 字幕表示 | -### 開発環境 +### キャラクター設定 -- Node.js: ^25.2.1 -- npm: ^11.6.2 +| 環境変数名 | 値 | 説明 | +| -------------------------------------- | ------------------------ | --------------- | +| `NEXT_PUBLIC_CHARACTER_NAME` | `全力肯定彼氏くん[LuC4]` | キャラクター名 | +| `NEXT_PUBLIC_MODEL_TYPE` | `vrm` | モデルタイプ | +| `NEXT_PUBLIC_SELECTED_VRM_PATH` | `/vrm/LuC4.vrm` | VRMファイルパス | +| `NEXT_PUBLIC_FIXED_CHARACTER_POSITION` | `true` | 位置固定 | +| `NEXT_PUBLIC_CHARACTER_POSITION_X` | `0.200` | X座標 | +| `NEXT_PUBLIC_CHARACTER_POSITION_Y` | `1.616` | Y座標 | +| `NEXT_PUBLIC_CHARACTER_POSITION_Z` | `1.455` | Z座標 | +| `NEXT_PUBLIC_CHARACTER_SCALE` | `1.000` | スケール | -### インストール手順 +### AI設定 -1. リポジトリをローカルにクローンします。 +| 環境変数名 | 値 | 説明 | +| ------------------------------- | ------------------ | ---------------- | +| `NEXT_PUBLIC_SELECT_AI_SERVICE` | `google` | AIサービス | +| `NEXT_PUBLIC_SELECT_AI_MODEL` | `gemini-2.0-flash` | AIモデル | +| `NEXT_PUBLIC_MAX_PAST_MESSAGES` | `10` | 会話履歴保持数 | +| `NEXT_PUBLIC_TEMPERATURE` | `0.7` | 応答のランダム性 | -```bash -git clone https://github.com/tegnike/aituber-kit.git -``` +### 音声設定 -2. フォルダを開きます。 +| 環境変数名 | 値 | 説明 | +| ----------------------------- | ----------------- | ---------------- | +| `NEXT_PUBLIC_SELECT_VOICE` | `google` | 音声合成エンジン | +| `NEXT_PUBLIC_GOOGLE_TTS_TYPE` | `ja-JP-Neural2-B` | 音声タイプ | -```bash -cd aituber-kit -``` +### スライドモード設定 -3. パッケージインストールします。 +| 環境変数名 | 値 | 説明 | +| -------------------------------- | ------------ | ------------------ | +| `NEXT_PUBLIC_SLIDE_MODE` | `true` | スライドモード有効 | +| `NEXT_PUBLIC_DEFAULT_SLIDE_DOCS` | `DHGSVR25-3` | デフォルトスライド | -```bash -npm install -``` +### アナリティクス・通知 -4. 必要に応じて.envファイルを作成します。 +| 環境変数名 | 値 | 説明 | +| ------------------------------- | ----------------------------- | ------------------- | +| `NEXT_PUBLIC_GA_MEASUREMENT_ID` | `G-XXXXXXXXXX` | Google Analytics ID | +| `SLACK_WEBHOOK_URL` | `https://hooks.slack.com/...` | Slack通知URL | -```bash -cp .env.example .env -``` +### その他 -5. 開発モードでアプリケーションを起動します。 +| 環境変数名 | 値 | 説明 | +| ------------------------------------------------ | -------------------------- | ------------ | +| `NEXT_PUBLIC_ALWAYS_OVERRIDE_WITH_ENV_VARIABLES` | `true` | 環境変数優先 | +| `NEXT_PUBLIC_BACKGROUND_IMAGE_PATH` | `/backgrounds/AITuber.png` | 背景画像 | -```bash -npm run dev +## 講義スライド + +| 回 | タイトル | フォルダ | +| ----- | ----------------------- | ---------------------------- | +| 第3回 | Webポートフォリオの制作 | `/public/slides/DHGSVR25-3/` | + +### スライド構成 + +``` +/public/slides/DHGSVR25-{回}/ +├── slides.md # Marp形式スライド +├── scripts.json # セリフデータ(感情タグ付き) +├── audio/ # 事前生成音声(MP3) +├── supplement.txt # Q&A用補足情報 +├── theme.css # カスタムテーマ +└── DHGS25Slides{n}.png # スライド画像 ``` -6. URLを開きます。[http://localhost:3000](http://localhost:3000) - -詳細な設定方法や使用方法については、[ドキュメントサイト](https://docs.aituberkit.com/)をご覧ください。 - -## ⚠️ セキュリティに関する重要な注意事項 - -このリポジトリは、個人利用やローカル環境での開発はもちろん、適切なセキュリティ対策を施した上での商用利用も想定しています。ただし、Web環境にデプロイする際は以下の点にご注意ください: - -- **APIキーの取り扱い**: バックエンドサーバーを経由してAIサービス(OpenAI, Anthropic等)やTTSサービスのAPIを呼び出す仕様となっているため、APIキーの適切な管理が必要です。 - -### 本番環境での利用について - -本番環境で利用する場合は、以下のいずれかの対応を推奨します: - -1. **バックエンドサーバーの実装**: APIキーの管理をサーバーサイドで行い、クライアントからの直接的なAPIアクセスを避ける -2. **利用者への適切な説明**: 各利用者が自身のAPIキーを使用する場合は、セキュリティ上の注意点について説明する -3. **アクセス制限の実装**: 必要に応じて、適切な認証・認可の仕組みを実装する - -## スポンサー募集 - -開発を継続するためにスポンサーの方を募集しています。
-あなたの支援は、AITuberKitの開発と改善に大きく貢献します。 - -[![GitHub Sponsor](https://img.shields.io/badge/Sponsor-GitHub-ea4aaa?style=for-the-badge&logo=github)](https://github.com/sponsors/tegnike) - -[!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://buymeacoffee.com/fdanv1k6iz) - -### 協力者の皆様(ご支援いただいた順) - -

- - morioki3 - - - hodachi-axcxept - - - coderabbitai - - - wmoto-ai - - - JunzoKamahara - - - darkgaldragon - - - usagi917 - - - ochisamu - - - mo0013 - - - tsubouchi - - - bunkaich - - - seiki-aliveland - - - rossy8417 - - - gijigae - - - takm-reason - - - haoling - - - FoundD-oka - - - terisuke - - - konpeita - - - MojaX2 - - - micchi99 - - - nekomeowww - - - yfuku - - - 8484ff_42 - - - sher1ock-jp - - - uwaguchi - - - M1RA_A_Project - - - teruPP - - - aituber-akari - - - harumeri - - - spring-hh - - - dotneet - - - schroneko - - - ParachutePenguin - - - eruma - - - _cityside - -

- -他、プライベートスポンサー 複数名 - -## Star History - -[![Star History Chart](https://api.star-history.com/svg?repos=tegnike/aituber-kit&type=Date)](https://star-history.com/#tegnike/aituber-kit&Date) - -## 謝辞 - -本プロジェクトは、pixiv株式会社が公開する [ChatVRM](https://github.com/pixiv/ChatVRM) をフォークして開発されています。素晴らしいオープンソースプロジェクトを公開してくださったpixiv株式会社に深く感謝いたします。 - -## 貢献 - -AITuberKitの発展にご協力いただき、ありがとうございます。コミュニティからの貢献を歓迎しています。 - -### イシューの報告 - -バグを見つけたり、新機能のアイデアがある場合は、GitHubの[Issues](https://github.com/tegnike/aituber-kit/issues)ページからぜひ教えてください。 - -イシューを作成する際に、以下の情報を含めていただけると対応がスムーズになります: - -- 問題や新機能の詳細な説明 -- 再現手順(バグの場合) -- 期待される動作と実際の動作 -- 使用環境(ブラウザ、OS、Node.jsのバージョンなど) -- スクリーンショットや動画(可能であれば) - -### プルリクエスト - -コードの改善や新機能の追加をしたい場合は、フォークしたリポジトリで変更を加え、プルリクエストを作成してください。 - -- 1つのプルリクエストでは、1つの機能または修正に焦点を当てるようにしてください。 -- プルリクエストの説明には、変更内容と理由を書いてください。 -- マージ先のブランチは必ず `develop` に設定してください。 -- コンフリクトは無理に解消しなくても問題ありません。開発チームが対応します。 - -## 利用規約 - -### ライセンス - -本プロジェクトは、バージョン v2.0.0 以降、**カスタムライセンス**を採用しています。 - -- **無償利用** - - 営利目的以外での個人利用、教育目的、非営利目的での使用は無償で利用可能です。 - -- **商用ライセンス** - - 商用目的での使用に関しては、別途商用ライセンスの取得が必要です。 - - 詳細は、[ライセンスについて](./docs/license.md)をご確認ください。 +## プレゼンテーションモード -### その他 +1. 設定画面(⚙️)を開く +2. **スライドモード** をオン +3. スライドフォルダを選択 +4. **開始** をクリック + +LuC4が自動でスライドを説明します。 + +### 機能 + +- 事前生成MP3音声の再生 +- 句読点で分割された字幕表示 +- Google Analytics でページ閲覧トラッキング +- 最終ページ到達時の Slack 通知 +- Ctrl+H でUIを非表示 + +## 本家 AITuber-kit について + +詳細な機能、設定、カスタマイズについては本家リポジトリを参照してください。 + +- **GitHub**: https://github.com/tegnike/aituber-kit +- **ドキュメント**: https://docs.aituberkit.com/ +- **デモサイト**: https://aituberkit.com + +### 主な機能 + +- AIキャラクターとの対話 +- VRM/Live2D キャラクターモデル +- 複数のLLMプロバイダー対応 +- 多彩な音声合成エンジン + +## ライセンス -- [ロゴの利用規約](./docs/logo_licence.md) -- [VRMおよびLive2Dモデルの利用規約](./docs/character_model_licence.md) +本家 AITuber-kit のライセンスに準拠します。 -## 優先実装について +- 非商用利用: 無料 +- 商用利用: 別途ライセンス必要 -本プロジェクトでは、有償での機能優先実装を受け付けています。 +詳細: https://github.com/tegnike/aituber-kit/blob/main/LICENSE -- 企業や個人の方から要望のあった機能を、優先的に実装することが可能です。 -- 実装された機能は、本OSSプロジェクトの一部として公開されます。 -- 料金は機能の複雑さや実装に要する時間に応じて個別見積もりとなります。 -- この優先実装は商用ライセンスとは別の取り組みです。実装された機能を商用利用する場合は、別途商用ライセンスの取得が必要です。 +## リンク -詳細については、support@aituberkit.com までお問い合わせください。 +- **デモサイト**: https://aituberkit.shirai.as/ +- **開発ブログ**: http://j.aicu.ai/s260103 +- **本家 AITuber-kit**: https://github.com/tegnike/aituber-kit +- **LuC4 公式**: https://luc4.aicu.jp/ +- **AICU Inc.**: https://corp.aicu.ai/ diff --git a/package.json b/package.json index 72546d517..1b5044cd4 100644 --- a/package.json +++ b/package.json @@ -114,7 +114,7 @@ "web-streams-polyfill": "^4.2.0" }, "engines": { - "node": "^25.2.1" + "node": "20.x" }, "volta": { "node": "25.2.1", diff --git a/public/backgrounds/AITuber.png b/public/backgrounds/AITuber.png new file mode 100644 index 000000000..3cf14e8f4 Binary files /dev/null and b/public/backgrounds/AITuber.png differ diff --git a/public/slides/DHGSVR25-3/DHGS25Slides1.png b/public/slides/DHGSVR25-3/DHGS25Slides1.png new file mode 100644 index 000000000..28c05becf Binary files /dev/null and b/public/slides/DHGSVR25-3/DHGS25Slides1.png differ diff --git a/public/slides/DHGSVR25-3/DHGS25Slides10.png b/public/slides/DHGSVR25-3/DHGS25Slides10.png new file mode 100644 index 000000000..f19b24286 Binary files /dev/null and b/public/slides/DHGSVR25-3/DHGS25Slides10.png differ diff --git a/public/slides/DHGSVR25-3/DHGS25Slides11.png b/public/slides/DHGSVR25-3/DHGS25Slides11.png new file mode 100644 index 000000000..38bc8067b Binary files /dev/null and b/public/slides/DHGSVR25-3/DHGS25Slides11.png differ diff --git a/public/slides/DHGSVR25-3/DHGS25Slides12.png b/public/slides/DHGSVR25-3/DHGS25Slides12.png new file mode 100644 index 000000000..ea1dab837 Binary files /dev/null and b/public/slides/DHGSVR25-3/DHGS25Slides12.png differ diff --git a/public/slides/DHGSVR25-3/DHGS25Slides13.png b/public/slides/DHGSVR25-3/DHGS25Slides13.png new file mode 100644 index 000000000..8553e8567 Binary files /dev/null and b/public/slides/DHGSVR25-3/DHGS25Slides13.png differ diff --git a/public/slides/DHGSVR25-3/DHGS25Slides14.png b/public/slides/DHGSVR25-3/DHGS25Slides14.png new file mode 100644 index 000000000..5f63452d7 Binary files /dev/null and b/public/slides/DHGSVR25-3/DHGS25Slides14.png differ diff --git a/public/slides/DHGSVR25-3/DHGS25Slides15.png b/public/slides/DHGSVR25-3/DHGS25Slides15.png new file mode 100644 index 000000000..2d1e79b0d Binary files /dev/null and b/public/slides/DHGSVR25-3/DHGS25Slides15.png differ diff --git a/public/slides/DHGSVR25-3/DHGS25Slides16.png b/public/slides/DHGSVR25-3/DHGS25Slides16.png new file mode 100644 index 000000000..93a692ef4 Binary files /dev/null and b/public/slides/DHGSVR25-3/DHGS25Slides16.png differ diff --git a/public/slides/DHGSVR25-3/DHGS25Slides17.png b/public/slides/DHGSVR25-3/DHGS25Slides17.png new file mode 100644 index 000000000..b61260a66 Binary files /dev/null and b/public/slides/DHGSVR25-3/DHGS25Slides17.png differ diff --git a/public/slides/DHGSVR25-3/DHGS25Slides18.png b/public/slides/DHGSVR25-3/DHGS25Slides18.png new file mode 100644 index 000000000..9e0574612 Binary files /dev/null and b/public/slides/DHGSVR25-3/DHGS25Slides18.png differ diff --git a/public/slides/DHGSVR25-3/DHGS25Slides19.png b/public/slides/DHGSVR25-3/DHGS25Slides19.png new file mode 100644 index 000000000..52a7526d7 Binary files /dev/null and b/public/slides/DHGSVR25-3/DHGS25Slides19.png differ diff --git a/public/slides/DHGSVR25-3/DHGS25Slides2.png b/public/slides/DHGSVR25-3/DHGS25Slides2.png new file mode 100644 index 000000000..ba3af67c9 Binary files /dev/null and b/public/slides/DHGSVR25-3/DHGS25Slides2.png differ diff --git a/public/slides/DHGSVR25-3/DHGS25Slides20.png b/public/slides/DHGSVR25-3/DHGS25Slides20.png new file mode 100644 index 000000000..4bc34108d Binary files /dev/null and b/public/slides/DHGSVR25-3/DHGS25Slides20.png differ diff --git a/public/slides/DHGSVR25-3/DHGS25Slides21.png b/public/slides/DHGSVR25-3/DHGS25Slides21.png new file mode 100644 index 000000000..0bfa430e6 Binary files /dev/null and b/public/slides/DHGSVR25-3/DHGS25Slides21.png differ diff --git a/public/slides/DHGSVR25-3/DHGS25Slides22.png b/public/slides/DHGSVR25-3/DHGS25Slides22.png new file mode 100644 index 000000000..e30dfec54 Binary files /dev/null and b/public/slides/DHGSVR25-3/DHGS25Slides22.png differ diff --git a/public/slides/DHGSVR25-3/DHGS25Slides23.png b/public/slides/DHGSVR25-3/DHGS25Slides23.png new file mode 100644 index 000000000..ce90bffd1 Binary files /dev/null and b/public/slides/DHGSVR25-3/DHGS25Slides23.png differ diff --git a/public/slides/DHGSVR25-3/DHGS25Slides24.png b/public/slides/DHGSVR25-3/DHGS25Slides24.png new file mode 100644 index 000000000..fbbc9cbe0 Binary files /dev/null and b/public/slides/DHGSVR25-3/DHGS25Slides24.png differ diff --git a/public/slides/DHGSVR25-3/DHGS25Slides25.png b/public/slides/DHGSVR25-3/DHGS25Slides25.png new file mode 100644 index 000000000..982e1b875 Binary files /dev/null and b/public/slides/DHGSVR25-3/DHGS25Slides25.png differ diff --git a/public/slides/DHGSVR25-3/DHGS25Slides26.png b/public/slides/DHGSVR25-3/DHGS25Slides26.png new file mode 100644 index 000000000..1c48c4622 Binary files /dev/null and b/public/slides/DHGSVR25-3/DHGS25Slides26.png differ diff --git a/public/slides/DHGSVR25-3/DHGS25Slides27.png b/public/slides/DHGSVR25-3/DHGS25Slides27.png new file mode 100644 index 000000000..25298771a Binary files /dev/null and b/public/slides/DHGSVR25-3/DHGS25Slides27.png differ diff --git a/public/slides/DHGSVR25-3/DHGS25Slides28.png b/public/slides/DHGSVR25-3/DHGS25Slides28.png new file mode 100644 index 000000000..93405aa88 Binary files /dev/null and b/public/slides/DHGSVR25-3/DHGS25Slides28.png differ diff --git a/public/slides/DHGSVR25-3/DHGS25Slides29.png b/public/slides/DHGSVR25-3/DHGS25Slides29.png new file mode 100644 index 000000000..e6b1890a3 Binary files /dev/null and b/public/slides/DHGSVR25-3/DHGS25Slides29.png differ diff --git a/public/slides/DHGSVR25-3/DHGS25Slides3.png b/public/slides/DHGSVR25-3/DHGS25Slides3.png new file mode 100644 index 000000000..938c21e49 Binary files /dev/null and b/public/slides/DHGSVR25-3/DHGS25Slides3.png differ diff --git a/public/slides/DHGSVR25-3/DHGS25Slides30.png b/public/slides/DHGSVR25-3/DHGS25Slides30.png new file mode 100644 index 000000000..3321a0f25 Binary files /dev/null and b/public/slides/DHGSVR25-3/DHGS25Slides30.png differ diff --git a/public/slides/DHGSVR25-3/DHGS25Slides31.png b/public/slides/DHGSVR25-3/DHGS25Slides31.png new file mode 100644 index 000000000..a0aa34e81 Binary files /dev/null and b/public/slides/DHGSVR25-3/DHGS25Slides31.png differ diff --git a/public/slides/DHGSVR25-3/DHGS25Slides32.png b/public/slides/DHGSVR25-3/DHGS25Slides32.png new file mode 100644 index 000000000..7a48a1acb Binary files /dev/null and b/public/slides/DHGSVR25-3/DHGS25Slides32.png differ diff --git a/public/slides/DHGSVR25-3/DHGS25Slides33.png b/public/slides/DHGSVR25-3/DHGS25Slides33.png new file mode 100644 index 000000000..bce53790b Binary files /dev/null and b/public/slides/DHGSVR25-3/DHGS25Slides33.png differ diff --git a/public/slides/DHGSVR25-3/DHGS25Slides34.png b/public/slides/DHGSVR25-3/DHGS25Slides34.png new file mode 100644 index 000000000..430fb2cca Binary files /dev/null and b/public/slides/DHGSVR25-3/DHGS25Slides34.png differ diff --git a/public/slides/DHGSVR25-3/DHGS25Slides35.png b/public/slides/DHGSVR25-3/DHGS25Slides35.png new file mode 100644 index 000000000..5b5d0616c Binary files /dev/null and b/public/slides/DHGSVR25-3/DHGS25Slides35.png differ diff --git a/public/slides/DHGSVR25-3/DHGS25Slides4.png b/public/slides/DHGSVR25-3/DHGS25Slides4.png new file mode 100644 index 000000000..23a49b917 Binary files /dev/null and b/public/slides/DHGSVR25-3/DHGS25Slides4.png differ diff --git a/public/slides/DHGSVR25-3/DHGS25Slides5.png b/public/slides/DHGSVR25-3/DHGS25Slides5.png new file mode 100644 index 000000000..ea347503f Binary files /dev/null and b/public/slides/DHGSVR25-3/DHGS25Slides5.png differ diff --git a/public/slides/DHGSVR25-3/DHGS25Slides6.png b/public/slides/DHGSVR25-3/DHGS25Slides6.png new file mode 100644 index 000000000..2e0aa3237 Binary files /dev/null and b/public/slides/DHGSVR25-3/DHGS25Slides6.png differ diff --git a/public/slides/DHGSVR25-3/DHGS25Slides7.png b/public/slides/DHGSVR25-3/DHGS25Slides7.png new file mode 100644 index 000000000..a0188389d Binary files /dev/null and b/public/slides/DHGSVR25-3/DHGS25Slides7.png differ diff --git a/public/slides/DHGSVR25-3/DHGS25Slides8.png b/public/slides/DHGSVR25-3/DHGS25Slides8.png new file mode 100644 index 000000000..5a81c59c8 Binary files /dev/null and b/public/slides/DHGSVR25-3/DHGS25Slides8.png differ diff --git a/public/slides/DHGSVR25-3/DHGS25Slides9.png b/public/slides/DHGSVR25-3/DHGS25Slides9.png new file mode 100644 index 000000000..4520683a9 Binary files /dev/null and b/public/slides/DHGSVR25-3/DHGS25Slides9.png differ diff --git a/public/slides/DHGSVR25-3/DHGSVR25-3-GitHub.pdf b/public/slides/DHGSVR25-3/DHGSVR25-3-GitHub.pdf new file mode 100644 index 000000000..280ea5ecb Binary files /dev/null and b/public/slides/DHGSVR25-3/DHGSVR25-3-GitHub.pdf differ diff --git a/public/slides/DHGSVR25-3/DHGSVR25-3.rtf b/public/slides/DHGSVR25-3/DHGSVR25-3.rtf new file mode 100644 index 000000000..8893481c1 --- /dev/null +++ b/public/slides/DHGSVR25-3/DHGSVR25-3.rtf @@ -0,0 +1,175 @@ +{\rtf1\ansi\ansicpg932\lnbrkrule +{\fonttbl +{\f1\fnil\fcharset128\fprq0 Dela Gothic One;} +{\f2\fnil\fcharset0\fprq0 Arial;} +{\f3\fnil\fcharset128\fprq0 Noto Sans JP;} +{\f4\fnil\fcharset128\fprq0 Kosugi Maru;} +{\f5\fnil\fcharset128\fprq0 Kosugi;} +{\f6\fnil\fcharset1\fprq0 Arial;} +} +{\colortbl; +\red0\green0\blue0; +\red89\green89\blue89; +\red0\green151\blue167; +\red255\green171\blue64; +\red0\green0\blue255; +} +{\stylesheet +{\s1\loch\af2\dbch\f2\fs28\cf1\lang1041\level1 heading 1;} +{\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\level2 heading 2;} +{\s3\li1440\loch\af2\dbch\f2\fs28\cf1\lang1041\level3 heading 3;} +{\s4\li2160\loch\af2\dbch\f2\fs28\cf1\lang1041\level4 heading 4;} +{\s5\li2880\loch\af2\dbch\f2\fs28\cf1\lang1041\level5 heading 5;} +{\s6\li3600\loch\af2\dbch\f2\fs28\cf1\lang1041\level6 heading 6;} +{\s7\li4320\loch\af2\dbch\f2\fs28\cf1\lang1041\level7 heading 7;} +{\s8\li5040\loch\af2\dbch\f2\fs28\cf1\lang1041\level8 heading 8;} +{\s9\li5760\loch\af2\dbch\f2\fs28\cf1\lang1041\level9 heading 9;} +} +\pard\plain\ltrpar\s1\loch\af2\dbch\f2\fs28\cf1\lang1041\qc {\loch\af1\dbch\f1\fs104\lang17 \'91\'e6}{\dbch\af1\loch\f1\fs104\lang1033\langfe17 3}{\loch\af1\dbch\f1\fs104\lang17 \'89\'f1\line }{\dbch\af1\loch\f1\fs104\lang1033\langfe17 2025}{\loch\af1\dbch\f1\fs104\lang17 \'94N}{\dbch\af1\loch\f1\fs104\lang1033\langfe17 12}{\loch\af1\dbch\f1\fs104\lang17 \'8c\'8e}{\dbch\af1\loch\f1\fs104\lang1033\langfe17 12}{\loch\af1\dbch\f1\fs104\lang17 \'93\'fa\'81i\'8b\'e0\'81j\line }{\dbch\af1\loch\f1\fs104\lang1033\langfe17 8}{\loch\af1\dbch\f1\fs104\lang17 \'8c\'c0\'81@}{\dbch\af1\loch\f1\fs104\lang1033\langfe17 21:00 }{\loch\af1\dbch\f1\fs104\lang17 \'81` }{\dbch\af1\loch\f1\fs104\lang1033\langfe17 22:30}{\dbch\af1\loch\f1\fs104\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0\qc {\loch\af3\dbch\f3\fs56\lang17 \'91\'e6}{\dbch\af3\loch\f3\fs56\lang1033\langfe17 3}{\loch\af3\dbch\f3\fs56\lang17 \'89\'f1 }{\dbch\af3\loch\f3\fs56\lang1033\langfe17 Web}{\loch\af3\dbch\f3\fs56\lang17 \'83|\'81[\'83g\'83t\'83H\'83\'8a\'83I\'82\'cc\'90\'a7\'8d\'ec}{\dbch\af3\loch\f3\fs56\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0\qc {\loch\af3\dbch\f3\fs56\lang17 \'83I\'81[\'83v\'83\'93\'83\'5c\'81[\'83X\'8bZ\'8fp\'82\'c6}{\dbch\af3\loch\f3\fs56\lang1033\langfe17 GitHub}{\loch\af3\dbch\f3\fs56\lang17 \'82\'f0\'8eg\'82\'c1\'82\'c4\'81A}{\dbch\af3\loch\f3\fs56\lang1033\langfe17 Web}{\loch\af3\dbch\f3\fs56\lang17 \'83\'81\'83^\'83o\'81[\'83X\'94N\'89\'ea\'8f\'f3\'82\'f0\'90\'a7\'8d\'ec\'82\'b5\'81A\'8c\'f6\'8aJ\'82\'b7\'82\'e9\'81B}{\dbch\af3\loch\f3\fs56\lang1033 \par +}\pard\plain\ltrpar\s1\loch\af2\dbch\f2\fs28\cf1\lang1041 {\dbch\af4\loch\f4\fs56\lang1033\langfe17 [DHGSVR25] }{\loch\af4\dbch\f4\fs56\lang17 \'83e\'83N\'83m\'83\'8d\'83W\'81[\'93\'c1\'98_}{\dbch\af4\loch\f4\fs56\lang1033\langfe17 D}{\loch\af4\dbch\f4\fs56\lang17 \'81i\'90l\'8dH\'8c\'bb\'8e\'c0\'81j}{\dbch\af4\loch\f4\fs56\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\dbch\af5\loch\f5\fs50\cf2\lang1033\langfe17 [11/28]}{\loch\af5\dbch\f5\fs50\cf2\lang17 \'91\'e6}{\dbch\af5\loch\f5\fs50\cf2\lang1033\langfe17 1}{\loch\af5\dbch\f5\fs50\cf2\lang17 \'89\'f1 \'90l\'8dH\'8c\'bb\'8e\'c0\'8aT\'98_}{\dbch\af5\loch\f5\fs50\cf2\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\dbch\af5\loch\f5\fs50\cf2\lang1033\langfe17 [12/5] }{\loch\af5\dbch\f5\fs50\cf2\lang17 \'91\'e6}{\dbch\af5\loch\f5\fs50\cf2\lang1033\langfe17 2}{\loch\af5\dbch\f5\fs50\cf2\lang17 \'89\'f1 }{\dbch\af5\loch\f5\fs50\cf2\lang1033\langfe17 VR5.0: }{\loch\af5\dbch\f5\fs50\cf2\lang17 \'97\'df\'98a\'82\'cc\'83\'81\'83^\'83o\'81[\'83X\'82\'c6\'90l\'8dH\'8c\'bb\'8e\'c0}{\dbch\af5\loch\f5\fs50\cf2\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\dbch\af5\loch\f5\fs50\lang1033\langfe17 [12/12]}{\loch\af5\dbch\f5\fs50\lang17 \'91\'e6}{\dbch\af5\loch\f5\fs50\lang1033\langfe17 3}{\loch\af5\dbch\f5\fs50\lang17 \'89\'f1 }{\dbch\af5\loch\f5\fs50\lang1033\langfe17 Web}{\loch\af5\dbch\f5\fs50\lang17 \'83|\'81[\'83g\'83t\'83H\'83\'8a\'83I\'82\'cc\'90\'a7\'8d\'ec}{\dbch\af5\loch\f5\fs50\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\dbch\af5\loch\f5\fs50\lang1033\langfe17 [12/19]}{\loch\af5\dbch\f5\fs50\lang17 \'91\'e6}{\dbch\af5\loch\f5\fs50\lang1033\langfe17 4}{\loch\af5\dbch\f5\fs50\lang17 \'89\'f1 \'83N\'83\'8a\'83G\'83C\'83e\'83B\'83u}{\dbch\af5\loch\f5\fs50\lang1033\langfe17 AI}{\dbch\af5\loch\f5\fs50\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\dbch\af5\loch\f5\fs50\lang1033\langfe17 [12/26]}{\loch\af5\dbch\f5\fs50\lang17 \'91\'e6}{\dbch\af5\loch\f5\fs50\lang1033\langfe17 5}{\loch\af5\dbch\f5\fs50\lang17 \'89\'f1 }{\dbch\af5\loch\f5\fs50\lang1033\langfe17 VTuber}{\loch\af5\dbch\f5\fs50\lang17 \'82\'f0\'90\'dd\'8cv\'82\'b7\'82\'e9}{\dbch\af5\loch\f5\fs50\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\dbch\af5\loch\f5\fs50\lang1033\langfe17 [1/9] }{\loch\af5\dbch\f5\fs50\lang17 \'91\'e6}{\dbch\af5\loch\f5\fs50\lang1033\langfe17 6}{\loch\af5\dbch\f5\fs50\lang17 \'89\'f1 }{\dbch\af5\loch\f5\fs50\lang1033\langfe17 VTuber}{\loch\af5\dbch\f5\fs50\lang17 \'82\'f0\'94\'ad\'90M\'82\'b7\'82\'e9}{\dbch\af5\loch\f5\fs50\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\dbch\af5\loch\f5\fs50\lang1033\langfe17 [1/16] }{\loch\af5\dbch\f5\fs50\lang17 \'91\'e6}{\dbch\af5\loch\f5\fs50\lang1033\langfe17 7}{\loch\af5\dbch\f5\fs50\lang17 \'89\'f1 \'81u\'90l\'8dH\'8c\'bb\'8e\'c0\'81v\'82\'f0\'82\'c2\'82\'ad\'82\'e9}{\dbch\af5\loch\f5\fs50\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\dbch\af5\loch\f5\fs50\lang1033\langfe17 [1/23] }{\loch\af5\dbch\f5\fs50\lang17 \'91\'e6}{\dbch\af5\loch\f5\fs50\lang1033\langfe17 8}{\loch\af5\dbch\f5\fs50\lang17 \'89\'f1 \'92\'b4\'81i}{\dbch\af5\loch\f5\fs50\lang1033\langfe17 meta}{\loch\af5\dbch\f5\fs50\lang17 \'81j\'83\'81\'83^\'83o\'81[\'83X}{\dbch\af5\loch\f5\fs50\lang1033 \par +}\pard\plain\ltrpar\s1\loch\af2\dbch\f2\fs28\cf1\lang1041 {\dbch\af4\loch\f4\fs24\lang1033\langfe17 https://www.aicu.jp/post/matilda-251222}{\dbch\af4\loch\f4\fs24\lang1033 \par +}\pard\plain\ltrpar\s1\loch\af2\dbch\f2\fs28\cf1\lang1041 {\dbch\af4\loch\f4\fs56\lang1033\langfe17 [}{\loch\af4\dbch\f4\fs56\lang17 \'91\'e6}{\dbch\af4\loch\f4\fs56\lang1033\langfe17 2}{\loch\af4\dbch\f4\fs56\lang17 \'89\'f1}{\dbch\af4\loch\f4\fs56\lang1033\langfe17 ] VR5.0: }{\loch\af4\dbch\f4\fs56\lang17 \'97\'df\'98a\'82\'cc\'83\'81\'83^\'83o\'81[\'83X\'82\'c6\'90l\'8dH\'8c\'bb\'8e\'c0}{\dbch\af4\loch\f4\fs56\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\fi-540 {\pntext\pard\dbch\af5\loch\f5\fs36 \uc1\u8226_\tab}{\*\pn\pnlvlblt\pnf5\pnfs36{\pntxtb \uc1\u8226_}}{\loch\af5\dbch\f5\fs36\lang17 \'82\'dc\'82\'e9\'82\'c4\'82\'a1\'81[ }{\ul\loch\af5\dbch\f5\fs36\cf3\lang17 \'90l\'8dH\'8c\'bb\'8e\'c0}{\ul\dbch\af5\loch\f5\fs36\cf3\lang1033\langfe17 2025-G251TG2016-}{\ul\loch\af5\dbch\f5\fs36\cf3\lang17 \'82\'dc\'82\'e9\'82\'c4\'82\'a1\'81[}{\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\fi-540 {\pntext\pard\dbch\af5\loch\f5\fs36 \uc1\u8226_\tab}{\*\pn\pnlvlblt\pnf5\pnfs36{\pntxtb \uc1\u8226_}}{\loch\af5\dbch\f5\fs36\lang17 \'83`\'83F\'83\'93\'81@}{\ul\loch\af5\dbch\f5\fs36\cf3\lang17 \'90l\'8dH\'8c\'bb\'8e\'c0}{\ul\dbch\af5\loch\f5\fs36\cf3\lang1033\langfe17 2025-G251TG2036-}{\ul\loch\af5\dbch\f5\fs36\cf3\lang17 \'83`\'83F\'83\'93}{\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\fi-540 {\pntext\pard\plain\dbch\af5\loch\f5\fs36 \uc1\u8226_\tab}{\*\pn\pnlvlblt\pnf5\pnfs36{\pntxtb \uc1\u8226_}}{\ul\loch\af5\dbch\f5\fs36\cf3\lang17 \'90l\'8dH\'8c\'bb\'8e\'c0}{\ul\dbch\af5\loch\f5\fs36\cf3\lang1033\langfe17 2025-G251TG2025-}{\ul\loch\af5\dbch\f5\fs36\cf3\lang17 \'83W\'83A\'83\'93 }{\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\fi-540 {\pntext\pard\plain\dbch\af5\loch\f5\fs36 \uc1\u8226_\tab}{\*\pn\pnlvlblt\pnf5\pnfs36{\pntxtb \uc1\u8226_}}{\ul\loch\af5\dbch\f5\fs36\cf3\lang17 \'90l\'8dH\'8c\'bb\'8e\'c0}{\ul\dbch\af5\loch\f5\fs36\cf3\lang1033\langfe17 2025-G251TG2045-}{\ul\loch\af5\dbch\f5\fs36\cf3\lang17 \'83\'89\'82\'b3\'82\'f1}{\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\fi-540 {\pntext\pard\dbch\af5\loch\f5\fs36 \uc1\u8226_\tab}{\*\pn\pnlvlblt\pnf5\pnfs36{\pntxtb \uc1\u8226_}}{\dbch\af5\loch\f5\fs36\lang1033\langfe17 Vecci }{\ul\loch\af5\dbch\f5\fs36\cf3\lang17 \'89\'80\'89\'84\'8f\'b9\'8eu}{\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\fi-540 {\pntext\pard\dbch\af5\loch\f5\fs36 \uc1\u8226_\tab}{\*\pn\pnlvlblt\pnf5\pnfs36{\pntxtb \uc1\u8226_}}{\loch\af5\dbch\f5\fs36\lang17 \'8eR\'89\'ba\'8ev\'96\'e5}{\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\fi-540 {\pntext\pard\dbch\af5\loch\f5\fs36 \uc1\u8226_\tab}{\*\pn\pnlvlblt\pnf5\pnfs36{\pntxtb \uc1\u8226_}}{\dbch\af5\loch\f5\fs36\lang1033\langfe17 C426}{\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\fi-540 {\pntext\pard\dbch\af5\loch\f5\fs36 \uc1\u8226_\tab}{\*\pn\pnlvlblt\pnf5\pnfs36{\pntxtb \uc1\u8226_}}{\loch\af5\dbch\f5\fs36\lang17 \'83R\'83i\'83\'93\'81@}{\ul\loch\af5\dbch\f5\fs36\cf3\lang17 \'90l\'8dH\'8c\'bb\'8e\'c0}{\ul\dbch\af5\loch\f5\fs36\cf3\lang1033\langfe17 2025-G251TG2011-}{\ul\loch\af5\dbch\f5\fs36\cf3\lang17 \'83R\'83i\'83\'93}{\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\fi-540 {\pntext\pard\dbch\af5\loch\f5\fs36 \uc1\u8226_\tab}{\*\pn\pnlvlblt\pnf5\pnfs36{\pntxtb \uc1\u8226_}}{\loch\af5\dbch\f5\fs36\lang17 \'83t\'83\'8d\'83\'8a\'83A }{\ul\loch\af5\dbch\f5\fs36\cf3\lang17 \'90l\'8dH\'8c\'bb\'8e\'c0}{\ul\dbch\af5\loch\f5\fs36\cf3\lang1033\langfe17 2025-G251TG2021-}{\ul\loch\af5\dbch\f5\fs36\cf3\lang17 \'83t\'83\'8d\'83\'8a\'83A}{\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\fi-540 {\pntext\pard\dbch\af5\loch\f5\fs36 \uc1\u8226_\tab}{\*\pn\pnlvlblt\pnf5\pnfs36{\pntxtb \uc1\u8226_}}{\loch\af5\dbch\f5\fs36\lang17 \'82\'a2\'82\'b4\'82\'d7\'82\'e7}{\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\fi-540 {\pntext\pard\dbch\af5\loch\f5\fs36 \uc1\u8226_\tab}{\*\pn\pnlvlblt\pnf5\pnfs36{\pntxtb \uc1\u8226_}}{\loch\af5\dbch\f5\fs36\lang17 \'83J\'83c\'83\'7d\'83^}{\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\fi-540 {\pntext\pard\dbch\af5\loch\f5\fs36 \uc1\u8226_\tab}{\*\pn\pnlvlblt\pnf5\pnfs36{\pntxtb \uc1\u8226_}}{\loch\af5\dbch\f5\fs36\lang17 \'82\'d9\'82\'f1\'82\'dc}{\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\fi-540 {\pntext\pard\dbch\af5\loch\f5\fs36 \uc1\u8226_\tab}{\*\pn\pnlvlblt\pnf5\pnfs36{\pntxtb \uc1\u8226_}}{\loch\af5\dbch\f5\fs36\lang17 \'83V\'83\'93}{\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\fi-540 {\pntext\pard\dbch\af5\loch\f5\fs36 \uc1\u8226_\tab}{\*\pn\pnlvlblt\pnf5\pnfs36{\pntxtb \uc1\u8226_}}{\loch\af5\dbch\f5\fs36\lang17 \'82\'e0\'82\'e8\'82\'bd}{\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\fi-540 {\pntext\pard\dbch\af5\loch\f5\fs36 \uc1\u8226_\tab}{\*\pn\pnlvlblt\pnf5\pnfs36{\pntxtb \uc1\u8226_}}{\loch\af5\dbch\f5\fs36\lang17 \'91\'90\'96\'ec\'82\'b3\'82\'f1}{\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s1\loch\af2\dbch\f2\fs28\cf1\lang1041 {\ul\loch\af4\dbch\f4\fs56\cf3\lang17 \'83L\'83Y\'83i\'83A\'83C}{\dbch\af4\loch\f4\fs56\lang1033 \par +}\pard\plain\ltrpar\s1\loch\af2\dbch\f2\fs28\cf1\lang1041 {\ul\dbch\af4\loch\f4\fs56\cf3\lang1033\langfe17 Google}{\ul\loch\af4\dbch\f4\fs56\cf3\lang17 \'82\'c6\'82\'cc\'83^\'83C\'83A\'83b\'83v\'8dL\'8d\'90}{\dbch\af4\loch\f4\fs56\lang1033 \par +}\pard\plain\ltrpar\s1\loch\af2\dbch\f2\fs28\cf1\lang1041 {\dbch\af2\loch\f2 \par +}\pard\plain\ltrpar\s1\loch\af2\dbch\f2\fs28\cf1\lang1041 {\ul\dbch\af4\loch\f4\fs34\cf3\lang1033\langfe17 https://j.aicu.ai/251212}{\dbch\af4\loch\f4\fs34\lang1033 \par +}\pard\plain\ltrpar\s1\loch\af2\dbch\f2\fs28\cf1\lang1041 {\dbch\af4\loch\f4\fs34\lang1033 \par +}\pard\plain\ltrpar\s1\loch\af2\dbch\f2\fs28\cf1\lang1041 {\loch\af4\dbch\f4\fs56\lang17 \'83f\'83B\'83Y\'83j\'81[\'81u\'94\'92\'90\'e1\'95P\'81v}{\dbch\af4\loch\f4\fs56\lang1033 \par +}\pard\plain\ltrpar\s1\loch\af2\dbch\f2\fs28\cf1\lang1041 {\loch\af4\dbch\f4\fs56\lang17 \'91\'e6}{\dbch\af4\loch\f4\fs56\lang1033\langfe17 3}{\loch\af4\dbch\f4\fs56\lang17 \'89\'f1 }{\dbch\af4\loch\f4\fs56\lang1033\langfe17 Web}{\loch\af4\dbch\f4\fs56\lang17 \'83|\'81[\'83g\'83t\'83H\'83\'8a\'83I\'82\'cc\'90\'a7\'8d\'ec}{\dbch\af4\loch\f4\fs56\lang1033\langfe17 (1)}{\dbch\af4\loch\f4\fs56\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\loch\af5\dbch\f5\fs36\lang17 \'83I\'81[\'83v\'83\'93\'83\'5c\'81[\'83X\'8bZ\'8fp\'82\'c6}{\dbch\af5\loch\f5\fs36\lang1033\langfe17 GitHub}{\loch\af5\dbch\f5\fs36\lang17 \'82\'f0\'8eg\'82\'c1\'82\'c4\'81A}{\dbch\af5\loch\f5\fs36\lang1033\langfe17 Web}{\loch\af5\dbch\f5\fs36\lang17 \'83\'81\'83^\'83o\'81[\'83X\'94N\'89\'ea\'8f\'f3\'82\'f0\'90\'a7\'8d\'ec\'82\'b5\'81A\'8c\'f6\'8aJ\'82\'b7\'82\'e9\'81B}{\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\loch\af5\dbch\f5\fs36\lang1033\langfe17 \'87@GitHub}{\loch\af5\dbch\f5\fs36\lang17 \'83A\'83J\'83E\'83\'93\'83g\'82\'f0\'8d\'ec\'82\'e9}{\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\loch\af5\dbch\f5\fs36\lang1033\langfe17 \'87AGitHub Pages}{\loch\af5\dbch\f5\fs36\lang17 \'82\'f0\'8eg\'82\'a4}{\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\loch\af5\dbch\f5\fs36\lang17 \'81@\'83\'8a\'83|\'83W\'83g\'83\'8a\'82\'cc\'8d\'ec\'90\'ac}{\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\loch\af5\dbch\f5\fs36\lang17 \'81@}{\dbch\af5\loch\f5\fs36\lang1033\langfe17 GitHub Desktop}{\loch\af5\dbch\f5\fs36\lang17 \'82\'cc\'83C\'83\'93\'83X\'83g\'81[\'83\'8b\'81@}{\ul\dbch\af5\loch\f5\fs36\cf3\lang1033\langfe17 https://github.com/apps/deskto}{\ul\dbch\af5\loch\f5\fs36\cf3\lang1033\langfe17 p}{\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\loch\af5\dbch\f5\fs36\lang17 \'81@}{\dbch\af5\loch\f5\fs36\lang1033\langfe17 VS Code }{\loch\af5\dbch\f5\fs36\lang17 \'82\'cc\'83C\'83\'93\'83X\'83g\'81[\'83\'8b\'81@}{\ul\dbch\af5\loch\f5\fs36\cf3\lang1033\langfe17 https://code.visualstudio.com/download}{\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\loch\af5\dbch\f5\fs36\lang1033\langfe17 \'87BAITuberKit}{\loch\af5\dbch\f5\fs36\lang17 \'82\'f0\'83N\'83\'8d\'81[\'83\'93\'82\'b7\'82\'e9}{\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\loch\af5\dbch\f5\fs36\lang1033\langfe17 \'87CChatGPT}{\loch\af5\dbch\f5\fs36\lang17 \'82\'cc}{\dbch\af5\loch\f5\fs36\lang1033\langfe17 API}{\loch\af5\dbch\f5\fs36\lang17 \'82\'f0\'8e\'e6\'93\'be\'82\'b7\'82\'e9}{\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\loch\af5\dbch\f5\fs36\lang1033\langfe17 \'87DGitHub}{\loch\af5\dbch\f5\fs36\lang17 \'82\'c9\'83f\'83v\'83\'8d\'83C\'82\'b7\'82\'e9}{\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s1\loch\af2\dbch\f2\fs28\cf1\lang1041 {\dbch\af4\loch\f4\fs56\lang1033\langfe17 GitHub}{\loch\af4\dbch\f4\fs56\lang17 \'83A\'83J\'83E\'83\'93\'83g\'82\'f0\'82\'c2\'82\'ad\'82\'e9}{\dbch\af4\loch\f4\fs56\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\ul\dbch\af5\loch\f5\fs36\cf3\lang1033\langfe17 https://github.com/kaitas}{\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s1\loch\af2\dbch\f2\fs28\cf1\lang1041 {\dbch\af4\loch\f4\fs56\lang1033\langfe17 GitHub Pages}{\loch\af4\dbch\f4\fs56\lang17 \'82\'f0\'82\'c2\'82\'a9\'82\'a4 }{\ul\loch\af5\dbch\f5\fs36\cf3\lang17 \'8c\'f6\'8e\'ae\'83\'7d\'83j\'83\'85\'83A\'83\'8b}{\dbch\af4\loch\f4\fs56\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\dbch\af5\loch\f5\fs36\lang1033\langfe17 Repositories \'81\'a8 New }{\ul\loch\af5\dbch\f5\fs36\cf3\lang17 \'8c\'f6\'8e\'ae\'83\'7d\'83j\'83\'85\'83A\'83\'8b}{\loch\af5\dbch\f5\fs36\lang17 \'81@}{\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s1\loch\af2\dbch\f2\fs28\cf1\lang1041 {\dbch\af4\loch\f4\fs56\lang1033\langfe17 GitHub Pages}{\loch\af4\dbch\f4\fs56\lang17 \'82\'f0\'82\'c2\'82\'a9\'82\'a4 }{\ul\loch\af5\dbch\f5\fs36\cf3\lang17 \'8c\'f6\'8e\'ae\'83\'7d\'83j\'83\'85\'83A\'83\'8b}{\dbch\af4\loch\f4\fs56\lang1033 \par +}\pard\plain\ltrpar\s1\loch\af2\dbch\f2\fs28\cf1\lang1041 {\dbch\af4\loch\f4\fs56\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\loch\af5\dbch\f5\fs36\lang17 \'83\'8a\'83|\'83W\'83g\'83\'8a\'82\'c9 }{\dbch\af5\loch\f5\fs36\lang1033\langfe17 .}{\ul\dbch\af5\loch\f5\fs36\cf3\lang1033\langfe17 github.io}{\dbch\af5\loch\f5\fs36\lang17 \'82\'c6\'82\'a2\'82\'a4\'96\'bc\'91O\'82\'f0\'95t\'82\'af\'82\'e9\'95K\'97v\'82\'aa\'82\'a0\'82\'e8\'82\'dc\'82\'b7\'81B}{\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s1\loch\af2\dbch\f2\fs28\cf1\lang1041 {\dbch\af4\loch\f4\fs56\lang1033\langfe17 GitHub Pages}{\loch\af4\dbch\f4\fs56\lang17 \'82\'f0\'82\'c2\'82\'a9\'82\'a4 }{\ul\loch\af5\dbch\f5\fs36\cf3\lang17 \'8c\'f6\'8e\'ae\'83\'7d\'83j\'83\'85\'83A\'83\'8b}{\dbch\af4\loch\f4\fs56\lang1033 \par +}\pard\plain\ltrpar\s1\loch\af2\dbch\f2\fs28\cf1\lang1041 {\dbch\af4\loch\f4\fs56\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\loch\af5\dbch\f5\fs36\lang17 \'83\'8a\'83|\'83W\'83g\'83\'8a\'82\'c9 }{\dbch\af5\loch\f5\fs36\lang1033\langfe17 .}{\ul\dbch\af5\loch\f5\fs36\cf3\lang1033\langfe17 github.io}{\dbch\af5\loch\f5\fs36\lang17 \'82\'c6\'82\'a2\'82\'a4\'96\'bc\'91O\'82\'f0\'95t\'82\'af\'82\'e9\'95K\'97v\'82\'aa\'82\'a0\'82\'e8\'82\'dc\'82\'b7\'81B}{\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s1\loch\af2\dbch\f2\fs28\cf1\lang1041 {\dbch\af2\loch\f2 \par +}\pard\plain\ltrpar\s1\loch\af2\dbch\f2\fs28\cf1\lang1041 {\dbch\af2\loch\f2 \par +}\pard\plain\ltrpar\s1\loch\af2\dbch\f2\fs28\cf1\lang1041 {\dbch\af4\loch\f4\fs56\lang1033\langfe17 GitHub Actions}{\loch\af4\dbch\f4\fs56\lang17 \'82\'c9\'82\'e6\'82\'e9\'83r\'83\'8b\'83h\'82\'aa\'8en\'82\'dc\'82\'e9}{\dbch\af4\loch\f4\fs56\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\loch\af5\dbch\f5\fs36\lang17 \'8e\'a9\'95\'aa\'82\'cc\'83\'8a\'83|\'83W\'83g\'83\'8a\'82\'cc}{\dbch\af5\loch\f5\fs36\lang1033\langfe17 [Actions]}{\loch\af5\dbch\f5\fs36\lang17 \'82\'cc\'83^\'83u\'82\'f0\'8c\'a9\'82\'e9\'82\'c6\uc1\u-10179_\uc1\u-8223_\'82\'ae\'82\'e9\'82\'ae\'82\'e9\'82\'b5\'82\'c4\'82\'a2\'82\'e9}{\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s1\loch\af2\dbch\f2\fs28\cf1\lang1041 {\loch\af4\dbch\f4\fs56\lang17 \'82\'b5\'82\'ce\'82\'e7\'82\'ad\'91\'d2\'82\'c2\'82\'c6\'8a\'ae\'90\'ac}{\dbch\af4\loch\f4\fs56\lang1033\langfe17 !}{\dbch\af4\loch\f4\fs56\lang1033 \par +}\pard\plain\ltrpar\s1\loch\af2\dbch\f2\fs28\cf1\lang1041 {\dbch\af4\loch\f4\fs56\lang1033\langfe17 GitHub Pages}{\loch\af4\dbch\f4\fs56\lang17 \'82\'f0\'82\'c2\'82\'a9\'82\'a4 }{\ul\loch\af5\dbch\f5\fs36\cf3\lang17 \'8c\'f6\'8e\'ae\'83\'7d\'83j\'83\'85\'83A\'83\'8b}{\dbch\af4\loch\f4\fs56\lang1033 \par +}\pard\plain\ltrpar\s1\loch\af2\dbch\f2\fs28\cf1\lang1041 {\dbch\af4\loch\f4\fs56\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\loch\af5\dbch\f5\fs36\lang17 \'82\'b1\'82\'cc\'8f\'f3\'91\'d4\'82\'be\'82\'c6 }{\dbch\af5\loch\f5\fs36\lang1033\langfe17 README.md }{\loch\af5\dbch\f5\fs36\lang17 \'82\'cc\'82\'dd\'82\'aa\'82\'a0\'82\'e9\'8f\'f3\'91\'d4\'82\'c8\'82\'cc\'82\'c5\'81A\'97\'d7\'82\'c9 }{\dbch\af5\loch\f5\fs36\lang1033\langfe17 index.html }{\loch\af5\dbch\f5\fs36\lang17 \'82\'f0\'8d\'ec\'82\'e9}{\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\loch\af5\dbch\f5\fs36\lang17 \'82\'dc\'82\'b8\'82\'cd\'83R\'81[\'83h\'83G\'83f\'83B\'83^\'82\'f0\'8eg\'82\'c1\'82\'c4 }{\dbch\af5\loch\f5\fs36\lang1033\langfe17 index.html }{\loch\af5\dbch\f5\fs36\lang17 \'82\'f0\'8e\'a9\'97\'cd\'82\'c5\'8d\'ec\'82\'c1\'82\'c4\'82\'dd\'82\'e6\'82\'a4\'81B}{\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\dbch\af5\loch\f5\fs36\lang17 \line \uc1\u11088_\uc1\u-497_\'82\'a9\'82\'c8\'82\'e8\'93K\'93\'96\'82\'c8}{\dbch\af5\loch\f5\fs36\lang1033\langfe17 HTML}{\loch\af5\dbch\f5\fs36\lang17 \'83t\'83@\'83C\'83\'8b\'82\'cc\'97\'e1}{\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\dbch\af5\loch\f5\fs36\lang1033\langfe17 }{\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\dbch\af5\loch\f5\fs36\lang17 }{\dbch\af5\loch\f5\fs36\lang1033\langfe17 }{\loch\af5\dbch\f5\fs36\lang17 \'96l\'82\'cc\'83|\'81[\'83g\'83t\'83H\'83\'8a\'83I }{\dbch\af5\loch\f5\fs36\lang1033\langfe17 }{\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\dbch\af5\loch\f5\fs36\lang1033\langfe17 }{\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s1\loch\af2\dbch\f2\fs28\cf1\lang1041 {\dbch\af4\loch\f4\fs56\lang1033\langfe17 GitHub Pages}{\loch\af4\dbch\f4\fs56\lang17 \'82\'f0\'82\'c2\'82\'a9\'82\'a4 }{\ul\loch\af5\dbch\f5\fs36\cf3\lang17 \'8c\'f6\'8e\'ae\'83\'7d\'83j\'83\'85\'83A\'83\'8b}{\dbch\af4\loch\f4\fs56\lang1033 \par +}\pard\plain\ltrpar\s1\loch\af2\dbch\f2\fs28\cf1\lang1041 {\dbch\af4\loch\f4\fs56\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\loch\af5\dbch\f5\fs36\lang17 \'82\'b1\'82\'cc\'8f\'f3\'91\'d4\'82\'be\'82\'c6 }{\ul\dbch\af5\loch\f5\fs36\cf3\lang1033\langfe17 README.md}{\dbch\af5\loch\f5\fs36\lang17 \'82\'cc\'82\'dd\'82\'aa\'82\'a0\'82\'e9\'8f\'f3\'91\'d4\'82\'c8\'82\'cc\'82\'c5\'81A\'97\'d7\'82\'c9 }{\dbch\af5\loch\f5\fs36\lang1033\langfe17 index.html }{\loch\af5\dbch\f5\fs36\lang17 \'82\'f0\'8d\'ec\'82\'e9}{\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\loch\af5\dbch\f5\fs36\lang17 \'81i\'82\'dc\'82\'b8\'82\'cd}{\dbch\af5\loch\f5\fs36\lang1033\langfe17 GitHub Desktop}{\loch\af5\dbch\f5\fs36\lang17 \'82\'c6}{\dbch\af5\loch\f5\fs36\lang1033\langfe17 VSCode}{\loch\af5\dbch\f5\fs36\lang17 \'82\'f0\'83C\'83\'93\'83X\'83g\'81[\'83\'8b\'82\'b5\'82\'bd\'8f\'f3\'91\'d4\'82\'c5\'81j\line }{\dbch\af5\loch\f5\fs36\lang1033\langfe17 [<>CODE]\'81\'a8[Open with GitHub Desktop]}{\loch\af5\dbch\f5\fs36\lang17 \'82\'c6\'82\'b7\'82\'e9\'82\'c6}{\dbch\af5\loch\f5\fs36\lang1033\langfe17 GitHub}{\loch\af5\dbch\f5\fs36\lang17 \'82\'aa\'8aJ\'82\'ad}{\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s1\loch\af2\dbch\f2\fs28\cf1\lang1041 {\dbch\af4\loch\f4\fs56\lang1033\langfe17 GitHub Desktop}{\loch\af4\dbch\f4\fs56\lang17 \'82\'c5\'82\'cc}{\dbch\af4\loch\f4\fs56\lang1033\langfe17 Clone}{\dbch\af4\loch\f4\fs56\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\loch\af5\dbch\f5\fs36\lang17 \'83N\'83\'8d\'81[\'83\'93\'82\'c6\'82\'cd\'81A}{\dbch\af5\loch\f5\fs36\lang1033\langfe17 GitHub}{\loch\af5\dbch\f5\fs36\lang17 \'82\'c5\'97a\'82\'a9\'82\'c1\'82\'c4\'82\'e0\'82\'e7\'82\'c1\'82\'c4\'82\'a2\'82\'e9\'83\'5c\'81[\'83X\'83R\'81[\'83h\'82\'cc\'83R\'83s\'81[\'82\'f0\'8d\'ec\'82\'c1\'82\'c4\line \'83\'8d\'81[\'83J\'83\'8b}{\dbch\af5\loch\f5\fs36\lang1033\langfe17 (=}{\loch\af5\dbch\f5\fs36\lang17 \'8e\'a9\'95\'aa\'82\'cc}{\dbch\af5\loch\f5\fs36\lang1033\langfe17 PC/Mac)}{\loch\af5\dbch\f5\fs36\lang17 \'82\'c5\'8aJ\'94\'ad\'82\'b5\'82\'bd\'82\'e8\'95\'d2\'8fW\'82\'c5\'82\'ab\'82\'e9\'8f\'f3\'91\'d4\'82\'c9\'82\'b7\'82\'e9\'82\'b1\'82\'c6\'81B}{\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\loch\af5\dbch\f5\fs36\lang17 \'83\'8d\'81[\'83J\'83\'8b\'82\'c5\'82\'cc\'8d\'ec\'8b\'c6\'82\'aa\'8fI\'82\'ed\'82\'c1\'82\'bd\'82\'e7\'81A\'83\'8a\'83|\'83W\'83g\'83\'8a}{\dbch\af5\loch\f5\fs36\lang1033\langfe17 (}{\loch\af5\dbch\f5\fs36\lang17 \'97a\'82\'a9\'82\'e8\'8f\'ea\'8f\'8a}{\dbch\af5\loch\f5\fs36\lang1033\langfe17 )}{\loch\af5\dbch\f5\fs36\lang17 \'82\'c9\'8d\'b7\'95\'aa\'82\'f0\'94[\'95i\'82\'b5\'82\'dc\'82\'b7\'81B}{\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\dbch\af5\loch\f5\fs36\lang1033\langfe17 Git}{\loch\af5\dbch\f5\fs36\lang17 \'82\'cd\'82\'bb\'82\'cc\'8d\'b7\'95\'aa\'82\'f0\line \'8f\'ed\'82\'c9\'8a\'c4\'8e\'8b\'82\'b5\'82\'c4\'8a\'c7\'97\'9d\'82\'b5\'82\'c4\'82\'ad\'82\'ea\'82\'e9\line \line \'95\'a1\'90\'94\'90l\'82\'c5\'82\'cc\'8aJ\'94\'ad\'82\'e2\line \'8d\'d7\'90\'d8\'82\'ea\'82\'cc\'8e\'9e\'8a\'d4\'82\'c5\'8aJ\'94\'ad\'82\'b7\'82\'e9\'8e\'9e\line \'91\'e5\'95\'cf\'96\'f0\'82\'c9\'97\'a7\'82\'bf\'82\'dc\'82\'b7\'81B}{\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s1\loch\af2\dbch\f2\fs28\cf1\lang1041 {\dbch\af4\loch\f4\fs56\lang1033\langfe17 GitHub Desktop}{\dbch\af4\loch\f4\fs56\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\loch\af4\dbch\f4\fs56\lang17 \'8d\'b6\'8f\'e3\'81u}{\dbch\af4\loch\f4\fs56\lang1033\langfe17 Current Repository}{\loch\af4\dbch\f4\fs56\lang17 \'81v\'82\'f0\'89E\'83N\'83\'8a\'83b\'83N}{\dbch\af4\loch\f4\fs56\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\dbch\af4\loch\f4\fs56\lang17 \'81\'a8\'81u}{\dbch\af4\loch\f4\fs56\lang1033\langfe17 Open in Visual Studio Code}{\loch\af4\dbch\f4\fs56\lang17 \'81v\'82\'c5\'8bN\'93\'ae\'82\'c5\'82\'ab\'82\'e9}{\dbch\af4\loch\f4\fs56\lang1033 \par +}\pard\plain\ltrpar\s1\loch\af2\dbch\f2\fs28\cf1\lang1041 {\dbch\af4\loch\f4\fs56\lang1033\langfe17 VS Code }{\loch\af4\dbch\f4\fs56\lang17 \'8f\'89\'89\'f1\'81i\'90M\'97\'8a\'82\'b7\'82\'e9\'81j}{\dbch\af4\loch\f4\fs56\lang1033 \par +}\pard\plain\ltrpar\s1\loch\af2\dbch\f2\fs28\cf1\lang1041 {\dbch\af4\loch\f4\fs56\lang1033\langfe17 Visual Studio Code}{\loch\af4\dbch\f4\fs56\lang17 \'82\'cc\'8eg\'82\'a2\'95\'fb\'82\'cd\'8a\'84\'88\'a4}{\dbch\af4\loch\f4\fs56\lang1033\langfe17 \'81c}{\loch\af4\dbch\f4\fs56\lang17 \'81I}{\dbch\af4\loch\f4\fs56\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\loch\af5\dbch\f5\fs36\lang17 \'8ao\'82\'a6\'82\'c4\'82\'a8\'82\'ad\'82\'c6\'82\'a2\'82\'a2\'82\'e0\'82\'cc\'81u\'8ag\'91\'e5\'81v\'81u\'83^\'81[\'83~\'83i\'83\'8b\'81v}{\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\loch\af5\dbch\f5\fs36\lang17 \'83t\'83@\'83C\'83\'8b}{\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\loch\af5\dbch\f5\fs36\lang17 \'8c\'9f\'8d\'f5}{\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\loch\af5\dbch\f5\fs36\lang17 \'83\'8a\'83|\'83W\'83g\'83\'8a}{\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\loch\af5\dbch\f5\fs36\lang17 \'82\'bb\'82\'cc\'89\'ba\'82\'cd\line \'83A\'83v\'83\'8a\'82\'c6\'82\'a9\line \'8b@\'94\'5c\'8ag\'92\'a3\'82\'c5\'82\'b7}{\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\loch\af5\dbch\f5\fs36\lang17 \'90\'dd\'92\'e8}{\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s1\loch\af2\dbch\f2\fs28\cf1\lang1041 {\dbch\af4\loch\f4\fs56\lang1033\langfe17 GitHub Desktop}{\loch\af4\dbch\f4\fs56\lang17 \'82\'c5\'82\'cc }{\dbch\af4\loch\f4\fs56\lang1033\langfe17 Commit & Push}{\dbch\af4\loch\f4\fs56\lang1033 \par +}\pard\plain\ltrpar\s1\loch\af2\dbch\f2\fs28\cf1\lang1041 {\dbch\af4\loch\f4\fs56\lang1033\langfe17 GitHub Desktop}{\loch\af4\dbch\f4\fs56\lang17 \'82\'c5\'82\'cc }{\dbch\af4\loch\f4\fs56\lang1033\langfe17 Commit & Push}{\dbch\af4\loch\f4\fs56\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\loch\af5\dbch\f5\fs36\lang17 \'95\'d2\'8fW\'82\'b5\'82\'c4\'82\'dd\'82\'e9}{\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\dbch\af5\loch\f5\fs36\lang1033\langfe17 README.md}{\loch\af5\dbch\f5\fs36\lang17 \'82\'f0\'89\'fc\'91\'a2}{\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\loch\af5\dbch\f5\fs36\lang17 \'83\'8a\'83\'93\'83N\'82\'cd}{\dbch\af5\loch\f5\fs36\lang1033\langfe17 [](URL)}{\loch\af5\dbch\f5\fs36\lang17 \'82\'c5\line \'82\'a9\'82\'af\'82\'dc\'82\'b7}{\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\loch\af5\dbch\f5\fs36\lang17 \'89\'e6\'91\'9c}{\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\dbch\af5\loch\f5\fs36\lang1033\langfe17 PNG}{\loch\af5\dbch\f5\fs36\lang17 \'83t\'83@\'83C\'83\'8b\'82\'f0}{\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\loch\af5\dbch\f5\fs36\lang17 \'93\'af\'8aK\'91w\'82\'c9\'82\'a8\'82\'ab\'82\'dc\'82\'b7}{\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\dbch\af5\loch\f5\fs36\lang1033\langfe17 (}{\loch\af5\dbch\f5\fs36\lang17 \'89E\'83N\'83\'8a\'83b\'83N\'82\'b5\'82\'c4\line }{\dbch\af5\loch\f5\fs36\lang1033\langfe17 Finder}{\loch\af5\dbch\f5\fs36\lang17 \'82\'c5\'95\'5c\'8e\'a6\'81j}{\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\loch\af5\dbch\f5\fs36\lang17 \'82\'e0\'82\'b5\'82\'ad\'82\'cd\line \'83G\'83N\'83X\'83v\'83\'8d\'81[\'83\'89\'81[\'82\'c9\line }{\dbch\af5\loch\f5\fs36\lang1033\langfe17 PNG}{\loch\af5\dbch\f5\fs36\lang17 \'83t\'83@\'83C\'83\'8b\'82\'f0\'83h\'83\'8d\'83b\'83v}{\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s1\loch\af2\dbch\f2\fs28\cf1\lang1041 {\dbch\af4\loch\f4\fs56\lang1033\langfe17 GitHub Desktop}{\loch\af4\dbch\f4\fs56\lang17 \'82\'c5\'82\'cc }{\dbch\af4\loch\f4\fs56\lang1033\langfe17 Commit & Push}{\dbch\af4\loch\f4\fs56\lang1033 \par +}\pard\plain\ltrpar\s1\loch\af2\dbch\f2\fs28\cf1\lang1041 {\dbch\af4\loch\f4\fs56\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\dbch\af5\loch\f5\fs36\lang1033\langfe17 GitHub Desktop}{\loch\af5\dbch\f5\fs36\lang17 \'82\'c9\'96\'df\'82\'e9\'82\'c6\'81A\'95\'cf\'8dX\'82\'aa\'8c\'9f\'8fo\'82\'b3\'82\'ea\'82\'c4\'82\'a2\'82\'e9\'82\'cd\'82\'b8\'82\'c5\'82\'b7\'81B}{\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\loch\af5\dbch\f5\fs36\lang17 \'89E\'91\'a4\'82\'c9\'95\'cf\'8dX\'89\'d3\'8f\'8a\'81i\'83e\'83L\'83X\'83g\'82\'cc\'8d\'b7\'95\'aa\'82\'aa}{\dbch\af5\loch\f5\fs36\lang1033\langfe17 +/-}{\loch\af5\dbch\f5\fs36\lang17 \'82\'c5\'8f\'91\'82\'a9\'82\'ea\'82\'c4\'82\'a2\'82\'e9\'81j}{\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\loch\af5\dbch\f5\fs36\lang17 \'8d\'b6\'91\'a4\'82\'aa\'91\'ce\'8f\'db\'83t\'83@\'83C\'83\'8b}{\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\loch\af5\dbch\f5\fs36\lang17 \'83R\'83~\'83b\'83g\'83\'81\'83b\'83Z\'81[\'83W}{\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\loch\af5\dbch\f5\fs36\lang17 \'82\'bb\'82\'cc\'8f\'da\'8d\'d7}{\dbch\af5\loch\f5\fs36\lang1033\langfe17 (}{\loch\af5\dbch\f5\fs36\lang17 \'95s\'97v}{\dbch\af5\loch\f5\fs36\lang1033\langfe17 )}{\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\dbch\af5\loch\f5\fs36\lang1033\langfe17 [Commit]}{\loch\af5\dbch\f5\fs36\lang17 \'82\'b7\'82\'e9}{\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s1\loch\af2\dbch\f2\fs28\cf1\lang1041 {\dbch\af4\loch\f4\fs56\lang1033\langfe17 GitHub Desktop}{\loch\af4\dbch\f4\fs56\lang17 \'82\'c5\'82\'cc }{\dbch\af4\loch\f4\fs56\lang1033\langfe17 Commit & Push}{\dbch\af4\loch\f4\fs56\lang1033 \par +}\pard\plain\ltrpar\s1\loch\af2\dbch\f2\fs28\cf1\lang1041 {\dbch\af4\loch\f4\fs56\lang1033 \par +}\pard\plain\ltrpar\s1\loch\af2\dbch\f2\fs28\cf1\lang1041 {\dbch\af4\loch\f4\fs56\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\dbch\af5\loch\f5\fs36\lang1033\langfe17 [Push origin]}{\loch\af5\dbch\f5\fs36\lang17 \'82\'c6\'82\'a2\'82\'a4\'83\'7b\'83^\'83\'93\'82\'f0\'89\'9f\'82\'b7\'82\'c6}{\dbch\af5\loch\f5\fs36\lang1033\langfe17 GitHub}{\loch\af5\dbch\f5\fs36\lang17 \'83T\'81[\'83o\'81[\'82\'c9\'94[\'95i\'8a\'ae\'97\'b9\'82\'c5\'82\'b7\'81I}{\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s1\loch\af2\dbch\f2\fs28\cf1\lang1041 {\loch\af4\dbch\f4\fs56\lang17 \'8d\'c4\'93x}{\dbch\af4\loch\f4\fs56\lang1033\langfe17 Web}{\loch\af4\dbch\f4\fs56\lang17 \'82\'cc}{\dbch\af4\loch\f4\fs56\lang1033\langfe17 GitHub Actions}{\loch\af4\dbch\f4\fs56\lang17 \'82\'f0\'8c\'a9\'82\'c9\'8ds\'82\'ad\'82\'c6}{\dbch\af4\loch\f4\fs56\lang1033\langfe17 \'81c}{\dbch\af4\loch\f4\fs56\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\loch\af5\dbch\f5\fs36\lang17 \'82\'dd\'82\'c8\'82\'b3\'82\'f1\'82\'cc\'8f\'ea\'8d\'87\'82\'cd\line }{\dbch\af5\loch\f5\fs36\lang1033\langfe17 (yourName).github.io/README.md\line }{\loch\af5\dbch\f5\fs36\lang17 \'82\'c6\'82\'a2\'82\'a4}{\dbch\af5\loch\f5\fs36\lang1033\langfe17 URL}{\loch\af5\dbch\f5\fs36\lang17 \'82\'f0\'8c\'a9\'82\'c9\'8ds\'82\'ad\'82\'c6\'82\'b1\'82\'a4\'82\'c8\'82\'c1\'82\'c4\'82\'e9}{\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\dbch\af5\loch\f5\fs36\lang1033\langfe17 github.io/(yourName)/README.md\line }{\loch\af5\dbch\f5\fs36\lang17 \'82\'c6\'82\'a2\'82\'a4}{\dbch\af5\loch\f5\fs36\lang1033\langfe17 URL}{\loch\af5\dbch\f5\fs36\lang17 \'82\'f0\'8c\'a9\'82\'c9\'8ds\'82\'ad\'82\'c6\'82\'b1\'82\'a4\'82\'c8\'82\'c1\'82\'c4\'82\'e9}{\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s1\loch\af2\dbch\f2\fs28\cf1\lang1041 {\dbch\af4\loch\f4\fs56\lang1033\langfe17 Codespace}{\loch\af4\dbch\f4\fs56\lang17 \'82\'f0\'8eg\'82\'a4\'8f\'ea\'8d\'87}{\dbch\af4\loch\f4\fs56\lang1033\langfe17 \'81c}{\dbch\af4\loch\f4\fs56\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\dbch\af5\loch\f5\fs36\lang1033\langfe17 GitHub}{\loch\af5\dbch\f5\fs36\lang17 \'83\'8a\'83|\'83W\'83g\'83\'8a\'82\'a9\'82\'e7\'81i}{\dbch\af5\loch\f5\fs36\lang1033\langfe17 GitHub Desktop}{\loch\af5\dbch\f5\fs36\lang17 \'82\'e0}{\dbch\af5\loch\f5\fs36\lang1033\langfe17 VS Code}{\loch\af5\dbch\f5\fs36\lang17 \'82\'e0\'8eg\'82\'ed\'82\'b8\'82\'c9\'81j\line \'92\'bc\'90\'da }{\dbch\af5\loch\f5\fs36\lang1033\langfe17 VS Code }{\loch\af5\dbch\f5\fs36\lang17 \'82\'c6\'93\'af\'82\'b6\'8a\'c2\'8b\'ab\'82\'f0\'8eg\'82\'a4\'82\'b1\'82\'c6\'82\'aa\'82\'c5\'82\'ab\'82\'e9\'81I}{\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s1\loch\af2\dbch\f2\fs28\cf1\lang1041 {\dbch\af2\loch\f2 \par +}\pard\plain\ltrpar\s1\loch\af2\dbch\f2\fs28\cf1\lang1041 {\loch\af4\dbch\f4\fs56\lang17 \'82\'c6\'82\'e0\'82\'a9\'82\'ad }{\dbch\af4\loch\f4\fs56\lang1033\langfe17 index.html }{\loch\af4\dbch\f4\fs56\lang17 \'82\'f0\'8d\'ec\'82\'eb\'82\'a4}{\dbch\af4\loch\f4\fs56\lang1033 \par +}\pard\plain\ltrpar\s1\loch\af2\dbch\f2\fs28\cf1\lang1041 {\dbch\af4\loch\f4\fs56\lang1033\langfe17 Codespace}{\loch\af4\dbch\f4\fs56\lang17 \'82\'c5\'82\'cc }{\dbch\af4\loch\f4\fs56\lang1033\langfe17 commit & push}{\dbch\af4\loch\f4\fs56\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\dbch\af5\loch\f5\fs36\lang1033\langfe17 GitHub}{\loch\af5\dbch\f5\fs36\lang17 \'83\'8a\'83|\'83W\'83g\'83\'8a\'82\'a9\'82\'e7\'81i}{\dbch\af5\loch\f5\fs36\lang1033\langfe17 GitHub Desktop}{\loch\af5\dbch\f5\fs36\lang17 \'82\'e0}{\dbch\af5\loch\f5\fs36\lang1033\langfe17 VS Code}{\loch\af5\dbch\f5\fs36\lang17 \'82\'e0\'8eg\'82\'ed\'82\'b8\'82\'c9\'81j\line \'92\'bc\'90\'da }{\dbch\af5\loch\f5\fs36\lang1033\langfe17 VS Code }{\loch\af5\dbch\f5\fs36\lang17 \'82\'c6\'93\'af\'82\'b6\'8a\'c2\'8b\'ab\'82\'f0\'8eg\'82\'a4\'82\'b1\'82\'c6\'82\'aa\'82\'c5\'82\'ab\'82\'e9\'81I}{\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s1\loch\af2\dbch\f2\fs28\cf1\lang1041 {\loch\af4\dbch\f4\fs56\lang17 \'8d\'a1\'8fT\'82\'cc\'89\'db\'91\'e8\'81u}{\dbch\af4\loch\f4\fs56\lang1033\langfe17 Codex}{\loch\af4\dbch\f4\fs56\lang17 \'81v\'82\'f0\'8eg\'82\'c1\'82\'c4}{\dbch\af4\loch\f4\fs56\lang1033\langfe17 Web}{\loch\af4\dbch\f4\fs56\lang17 \'83T\'83C\'83g\'82\'f0\'8d\'ec\'82\'e9}{\dbch\af4\loch\f4\fs56\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\ul\dbch\af5\loch\f5\fs36\cf3\lang1033\langfe17 https://github.com/kaitas/DHGSVR}{\dbch\af5\loch\f5\fs36\lang17 \'81\'a8\'81@}{\ul\dbch\af5\loch\f5\fs36\cf3\lang1033\langfe17 https://akihiko.shirai.as/DHGSVR/}{\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s2\li720\loch\af2\dbch\f2\fs28\cf1\lang1041\li0 {\dbch\af5\loch\f5\fs36\lang1033\langfe17 mac}{\loch\af5\dbch\f5\fs36\lang17 \'82\'cc\'8f\'ea\'8d\'87\'82\'cd }{\dbch\af5\loch\f5\fs36\cf4\lang1033\langfe17 brew install codex}{\dbch\af5\loch\f5\fs36\lang17 \'82\'c5}{\dbch\af5\loch\f5\fs36\lang1033\langfe17 OK}{\loch\af5\dbch\f5\fs36\lang17 \'81B}{\dbch\af5\loch\f5\fs36\lang1033\langfe17 ChatGPT Plus}{\loch\af5\dbch\f5\fs36\lang17 \'8c_\'96\'f1\'8e\'d2\'82\'cd\'96\'b3\'97\'bf\'82\'c5\'82\'b7\'81B}{\dbch\af5\loch\f5\fs36\lang1033 \par +}\pard\plain\ltrpar\s1\loch\af2\dbch\f2\fs28\cf1\lang1041 {\loch\af4\dbch\f4\fs56\lang17 \'82\'bb\'82\'cc\'82\'a0\'82\'c6\'82\'cd }{\dbch\af4\loch\f4\fs56\lang1033\langfe17 AITuber Kit }{\loch\af4\dbch\f4\fs56\lang17 \'8eg\'82\'c1\'82\'c4\'8e\'a9\'95\'aa\'82\'cc\'83T\'83C\'83g\'82\'f0\'81I}{\dbch\af4\loch\f4\fs56\lang1033 \par +} +} \ No newline at end of file diff --git a/public/slides/DHGSVR25-3/LuC4-1.png b/public/slides/DHGSVR25-3/LuC4-1.png new file mode 100644 index 000000000..6cd57a61d Binary files /dev/null and b/public/slides/DHGSVR25-3/LuC4-1.png differ diff --git a/public/slides/DHGSVR25-3/LuC4-2.png b/public/slides/DHGSVR25-3/LuC4-2.png new file mode 100644 index 000000000..2e0002e8d Binary files /dev/null and b/public/slides/DHGSVR25-3/LuC4-2.png differ diff --git a/public/slides/DHGSVR25-3/Qwen3tts-LuC4-aiden.wav b/public/slides/DHGSVR25-3/Qwen3tts-LuC4-aiden.wav new file mode 100644 index 000000000..ec894c352 Binary files /dev/null and b/public/slides/DHGSVR25-3/Qwen3tts-LuC4-aiden.wav differ diff --git a/public/slides/DHGSVR25-3/TTS-API.png b/public/slides/DHGSVR25-3/TTS-API.png new file mode 100644 index 000000000..fbab2ee79 Binary files /dev/null and b/public/slides/DHGSVR25-3/TTS-API.png differ diff --git a/public/slides/DHGSVR25-3/TTS.png b/public/slides/DHGSVR25-3/TTS.png new file mode 100644 index 000000000..480bcd916 Binary files /dev/null and b/public/slides/DHGSVR25-3/TTS.png differ diff --git a/public/slides/DHGSVR25-3/Vercel1.png b/public/slides/DHGSVR25-3/Vercel1.png new file mode 100644 index 000000000..e5b4c0d7f Binary files /dev/null and b/public/slides/DHGSVR25-3/Vercel1.png differ diff --git a/public/slides/DHGSVR25-3/Vercel2.png b/public/slides/DHGSVR25-3/Vercel2.png new file mode 100644 index 000000000..054429b0b Binary files /dev/null and b/public/slides/DHGSVR25-3/Vercel2.png differ diff --git a/public/slides/DHGSVR25-3/Vercel3.png b/public/slides/DHGSVR25-3/Vercel3.png new file mode 100644 index 000000000..49bca3927 Binary files /dev/null and b/public/slides/DHGSVR25-3/Vercel3.png differ diff --git a/public/slides/DHGSVR25-3/Vercel4.png b/public/slides/DHGSVR25-3/Vercel4.png new file mode 100644 index 000000000..08dd47830 Binary files /dev/null and b/public/slides/DHGSVR25-3/Vercel4.png differ diff --git a/public/slides/DHGSVR25-3/audio/page0.mp3 b/public/slides/DHGSVR25-3/audio/page0.mp3 new file mode 100644 index 000000000..7445afadf Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page0.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page1.mp3 b/public/slides/DHGSVR25-3/audio/page1.mp3 new file mode 100644 index 000000000..1e42ad94d Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page1.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page10.mp3 b/public/slides/DHGSVR25-3/audio/page10.mp3 new file mode 100644 index 000000000..6e3273bde Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page10.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page11.mp3 b/public/slides/DHGSVR25-3/audio/page11.mp3 new file mode 100644 index 000000000..8fb3cb60a Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page11.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page12.mp3 b/public/slides/DHGSVR25-3/audio/page12.mp3 new file mode 100644 index 000000000..1b2193393 Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page12.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page13.mp3 b/public/slides/DHGSVR25-3/audio/page13.mp3 new file mode 100644 index 000000000..ebb288646 Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page13.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page14.mp3 b/public/slides/DHGSVR25-3/audio/page14.mp3 new file mode 100644 index 000000000..4ab3d987d Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page14.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page15.mp3 b/public/slides/DHGSVR25-3/audio/page15.mp3 new file mode 100644 index 000000000..1bc3f5854 Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page15.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page16.mp3 b/public/slides/DHGSVR25-3/audio/page16.mp3 new file mode 100644 index 000000000..e41c4ec95 Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page16.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page17.mp3 b/public/slides/DHGSVR25-3/audio/page17.mp3 new file mode 100644 index 000000000..9b5a00297 Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page17.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page18.mp3 b/public/slides/DHGSVR25-3/audio/page18.mp3 new file mode 100644 index 000000000..d7e25402f Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page18.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page19.mp3 b/public/slides/DHGSVR25-3/audio/page19.mp3 new file mode 100644 index 000000000..fa5847248 Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page19.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page2.mp3 b/public/slides/DHGSVR25-3/audio/page2.mp3 new file mode 100644 index 000000000..0758fe24d Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page2.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page20.mp3 b/public/slides/DHGSVR25-3/audio/page20.mp3 new file mode 100644 index 000000000..56f08d720 Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page20.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page21.mp3 b/public/slides/DHGSVR25-3/audio/page21.mp3 new file mode 100644 index 000000000..3c635ade8 Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page21.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page22.mp3 b/public/slides/DHGSVR25-3/audio/page22.mp3 new file mode 100644 index 000000000..4776abbef Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page22.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page23.mp3 b/public/slides/DHGSVR25-3/audio/page23.mp3 new file mode 100644 index 000000000..02c75140f Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page23.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page24.mp3 b/public/slides/DHGSVR25-3/audio/page24.mp3 new file mode 100644 index 000000000..f5f68158b Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page24.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page25.mp3 b/public/slides/DHGSVR25-3/audio/page25.mp3 new file mode 100644 index 000000000..bdf992ef1 Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page25.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page26.mp3 b/public/slides/DHGSVR25-3/audio/page26.mp3 new file mode 100644 index 000000000..a98a891ed Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page26.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page27.mp3 b/public/slides/DHGSVR25-3/audio/page27.mp3 new file mode 100644 index 000000000..45ad76e3f Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page27.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page28.mp3 b/public/slides/DHGSVR25-3/audio/page28.mp3 new file mode 100644 index 000000000..4e07b230f Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page28.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page29.mp3 b/public/slides/DHGSVR25-3/audio/page29.mp3 new file mode 100644 index 000000000..f2fab3ce9 Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page29.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page3.mp3 b/public/slides/DHGSVR25-3/audio/page3.mp3 new file mode 100644 index 000000000..7123d857c Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page3.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page30.mp3 b/public/slides/DHGSVR25-3/audio/page30.mp3 new file mode 100644 index 000000000..d81cf9c34 Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page30.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page31.mp3 b/public/slides/DHGSVR25-3/audio/page31.mp3 new file mode 100644 index 000000000..bb07390ed Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page31.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page32.mp3 b/public/slides/DHGSVR25-3/audio/page32.mp3 new file mode 100644 index 000000000..177880953 Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page32.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page33.mp3 b/public/slides/DHGSVR25-3/audio/page33.mp3 new file mode 100644 index 000000000..d6992ab77 Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page33.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page34.mp3 b/public/slides/DHGSVR25-3/audio/page34.mp3 new file mode 100644 index 000000000..8d64b2ad7 Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page34.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page35.mp3 b/public/slides/DHGSVR25-3/audio/page35.mp3 new file mode 100644 index 000000000..47722ed33 Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page35.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page36.mp3 b/public/slides/DHGSVR25-3/audio/page36.mp3 new file mode 100644 index 000000000..ae2fe6531 Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page36.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page37.mp3 b/public/slides/DHGSVR25-3/audio/page37.mp3 new file mode 100644 index 000000000..02bbc6c03 Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page37.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page38.mp3 b/public/slides/DHGSVR25-3/audio/page38.mp3 new file mode 100644 index 000000000..27a63f578 Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page38.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page39.mp3 b/public/slides/DHGSVR25-3/audio/page39.mp3 new file mode 100644 index 000000000..a5c213f46 Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page39.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page4.mp3 b/public/slides/DHGSVR25-3/audio/page4.mp3 new file mode 100644 index 000000000..2593649c3 Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page4.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page40.mp3 b/public/slides/DHGSVR25-3/audio/page40.mp3 new file mode 100644 index 000000000..44b5c8594 Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page40.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page41.mp3 b/public/slides/DHGSVR25-3/audio/page41.mp3 new file mode 100644 index 000000000..8629fe181 Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page41.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page42.mp3 b/public/slides/DHGSVR25-3/audio/page42.mp3 new file mode 100644 index 000000000..5adc60e31 Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page42.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page43.mp3 b/public/slides/DHGSVR25-3/audio/page43.mp3 new file mode 100644 index 000000000..b9542a47d Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page43.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page44.mp3 b/public/slides/DHGSVR25-3/audio/page44.mp3 new file mode 100644 index 000000000..92ae5569b Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page44.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page45.mp3 b/public/slides/DHGSVR25-3/audio/page45.mp3 new file mode 100644 index 000000000..36358fc57 Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page45.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page46.mp3 b/public/slides/DHGSVR25-3/audio/page46.mp3 new file mode 100644 index 000000000..948ecdcbe Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page46.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page47.mp3 b/public/slides/DHGSVR25-3/audio/page47.mp3 new file mode 100644 index 000000000..7bd5486c1 Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page47.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page48.mp3 b/public/slides/DHGSVR25-3/audio/page48.mp3 new file mode 100644 index 000000000..97ad56d5b Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page48.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page49.mp3 b/public/slides/DHGSVR25-3/audio/page49.mp3 new file mode 100644 index 000000000..e8774e339 Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page49.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page5.mp3 b/public/slides/DHGSVR25-3/audio/page5.mp3 new file mode 100644 index 000000000..8a337cda0 Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page5.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page50.mp3 b/public/slides/DHGSVR25-3/audio/page50.mp3 new file mode 100644 index 000000000..55c34eb1a Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page50.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page51.mp3 b/public/slides/DHGSVR25-3/audio/page51.mp3 new file mode 100644 index 000000000..c0069f85b Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page51.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page52.mp3 b/public/slides/DHGSVR25-3/audio/page52.mp3 new file mode 100644 index 000000000..1652eda16 Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page52.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page53.mp3 b/public/slides/DHGSVR25-3/audio/page53.mp3 new file mode 100644 index 000000000..ef42f72f9 Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page53.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page54.mp3 b/public/slides/DHGSVR25-3/audio/page54.mp3 new file mode 100644 index 000000000..d0b08c366 Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page54.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page55.mp3 b/public/slides/DHGSVR25-3/audio/page55.mp3 new file mode 100644 index 000000000..2f2545808 Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page55.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page56.mp3 b/public/slides/DHGSVR25-3/audio/page56.mp3 new file mode 100644 index 000000000..af7862e0a Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page56.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page57.mp3 b/public/slides/DHGSVR25-3/audio/page57.mp3 new file mode 100644 index 000000000..7b2b187a2 Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page57.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page58.mp3 b/public/slides/DHGSVR25-3/audio/page58.mp3 new file mode 100644 index 000000000..1dcd48942 Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page58.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page59.mp3 b/public/slides/DHGSVR25-3/audio/page59.mp3 new file mode 100644 index 000000000..c25419351 Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page59.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page6.mp3 b/public/slides/DHGSVR25-3/audio/page6.mp3 new file mode 100644 index 000000000..35f8fac53 Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page6.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page60.mp3 b/public/slides/DHGSVR25-3/audio/page60.mp3 new file mode 100644 index 000000000..c6e175b18 Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page60.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page61.mp3 b/public/slides/DHGSVR25-3/audio/page61.mp3 new file mode 100644 index 000000000..552f11a57 Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page61.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page62.mp3 b/public/slides/DHGSVR25-3/audio/page62.mp3 new file mode 100644 index 000000000..9ccf51105 Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page62.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page63.mp3 b/public/slides/DHGSVR25-3/audio/page63.mp3 new file mode 100644 index 000000000..df407ad2f Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page63.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page64.mp3 b/public/slides/DHGSVR25-3/audio/page64.mp3 new file mode 100644 index 000000000..9097b7b71 Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page64.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page65.mp3 b/public/slides/DHGSVR25-3/audio/page65.mp3 new file mode 100644 index 000000000..47c7fdda2 Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page65.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page66.mp3 b/public/slides/DHGSVR25-3/audio/page66.mp3 new file mode 100644 index 000000000..3d50bf51e Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page66.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page67.mp3 b/public/slides/DHGSVR25-3/audio/page67.mp3 new file mode 100644 index 000000000..ae8e45081 Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page67.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page68.mp3 b/public/slides/DHGSVR25-3/audio/page68.mp3 new file mode 100644 index 000000000..9c19e82d8 Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page68.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page69.mp3 b/public/slides/DHGSVR25-3/audio/page69.mp3 new file mode 100644 index 000000000..eb4bd9d0a Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page69.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page7.mp3 b/public/slides/DHGSVR25-3/audio/page7.mp3 new file mode 100644 index 000000000..a9763d212 Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page7.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page70.mp3 b/public/slides/DHGSVR25-3/audio/page70.mp3 new file mode 100644 index 000000000..e133e018d Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page70.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page71.mp3 b/public/slides/DHGSVR25-3/audio/page71.mp3 new file mode 100644 index 000000000..c13508a35 Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page71.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page72.mp3 b/public/slides/DHGSVR25-3/audio/page72.mp3 new file mode 100644 index 000000000..973887358 Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page72.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page73.mp3 b/public/slides/DHGSVR25-3/audio/page73.mp3 new file mode 100644 index 000000000..f6c7393fa Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page73.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page74.mp3 b/public/slides/DHGSVR25-3/audio/page74.mp3 new file mode 100644 index 000000000..233b45735 Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page74.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page75.mp3 b/public/slides/DHGSVR25-3/audio/page75.mp3 new file mode 100644 index 000000000..394cf8a9d Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page75.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page76.mp3 b/public/slides/DHGSVR25-3/audio/page76.mp3 new file mode 100644 index 000000000..702672e9d Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page76.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page77.mp3 b/public/slides/DHGSVR25-3/audio/page77.mp3 new file mode 100644 index 000000000..388efc136 Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page77.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page78.mp3 b/public/slides/DHGSVR25-3/audio/page78.mp3 new file mode 100644 index 000000000..814788c4e Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page78.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page79.mp3 b/public/slides/DHGSVR25-3/audio/page79.mp3 new file mode 100644 index 000000000..ca5917cd6 Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page79.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page8.mp3 b/public/slides/DHGSVR25-3/audio/page8.mp3 new file mode 100644 index 000000000..284476a2b Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page8.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page80.mp3 b/public/slides/DHGSVR25-3/audio/page80.mp3 new file mode 100644 index 000000000..caee0a27d Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page80.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page81.mp3 b/public/slides/DHGSVR25-3/audio/page81.mp3 new file mode 100644 index 000000000..1e1c214a8 Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page81.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page82.mp3 b/public/slides/DHGSVR25-3/audio/page82.mp3 new file mode 100644 index 000000000..6590b378a Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page82.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page83.mp3 b/public/slides/DHGSVR25-3/audio/page83.mp3 new file mode 100644 index 000000000..83b612c81 Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page83.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page84.mp3 b/public/slides/DHGSVR25-3/audio/page84.mp3 new file mode 100644 index 000000000..5d0c1c9bb Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page84.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page85.mp3 b/public/slides/DHGSVR25-3/audio/page85.mp3 new file mode 100644 index 000000000..fbfb02b1c Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page85.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page86.mp3 b/public/slides/DHGSVR25-3/audio/page86.mp3 new file mode 100644 index 000000000..6f091eb0c Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page86.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page87.mp3 b/public/slides/DHGSVR25-3/audio/page87.mp3 new file mode 100644 index 000000000..92a5db700 Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page87.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page88.mp3 b/public/slides/DHGSVR25-3/audio/page88.mp3 new file mode 100644 index 000000000..bc1c87c1e Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page88.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page89.mp3 b/public/slides/DHGSVR25-3/audio/page89.mp3 new file mode 100644 index 000000000..bc78d9e42 Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page89.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page9.mp3 b/public/slides/DHGSVR25-3/audio/page9.mp3 new file mode 100644 index 000000000..fade8dfa6 Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page9.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page90.mp3 b/public/slides/DHGSVR25-3/audio/page90.mp3 new file mode 100644 index 000000000..0be864833 Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page90.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page91.mp3 b/public/slides/DHGSVR25-3/audio/page91.mp3 new file mode 100644 index 000000000..1c72482bf Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page91.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page92.mp3 b/public/slides/DHGSVR25-3/audio/page92.mp3 new file mode 100644 index 000000000..1b05ea603 Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page92.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page93.mp3 b/public/slides/DHGSVR25-3/audio/page93.mp3 new file mode 100644 index 000000000..5e4ba63c3 Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page93.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page94.mp3 b/public/slides/DHGSVR25-3/audio/page94.mp3 new file mode 100644 index 000000000..2d23ef0c4 Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page94.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page95.mp3 b/public/slides/DHGSVR25-3/audio/page95.mp3 new file mode 100644 index 000000000..518c065c5 Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page95.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page96.mp3 b/public/slides/DHGSVR25-3/audio/page96.mp3 new file mode 100644 index 000000000..266be48b0 Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page96.mp3 differ diff --git a/public/slides/DHGSVR25-3/audio/page97.mp3 b/public/slides/DHGSVR25-3/audio/page97.mp3 new file mode 100644 index 000000000..7b118aecd Binary files /dev/null and b/public/slides/DHGSVR25-3/audio/page97.mp3 differ diff --git a/public/slides/DHGSVR25-3/generate-audio-aicu.js b/public/slides/DHGSVR25-3/generate-audio-aicu.js new file mode 100644 index 000000000..df9bdd692 --- /dev/null +++ b/public/slides/DHGSVR25-3/generate-audio-aicu.js @@ -0,0 +1,234 @@ +#!/usr/bin/env node +/** + * scripts.json から AICU API で MP3 音声ファイルを一括生成するツール + * + * 使い方: + * node generate-audio-aicu.js [オプション] + * + * オプション: + * --all 全ページを生成(既存ファイルも上書き) + * --missing 存在しないページのみ生成(デフォルト) + * --page N 指定ページのみ生成 + * --range N-M 指定範囲のページを生成 + * --dry-run 実際には生成せず、対象ページを表示 + * + * 環境変数: + * AICU_API_KEY AICU API キー (aicu_ent_xxx or aicu_live_xxx) + * AICU_SLUG キャラクターslug (default: luc4) + * + * 例: + * AICU_API_KEY=aicu_ent_xxx node generate-audio-aicu.js --missing + * AICU_API_KEY=aicu_ent_xxx AICU_SLUG=luc4 node generate-audio-aicu.js --page 0 + * AICU_API_KEY=aicu_ent_xxx node generate-audio-aicu.js --range 40-50 + * node generate-audio-aicu.js --all --dry-run + */ + +const fs = require('fs') +const path = require('path') +const https = require('https') + +// 設定 +const SCRIPTS_PATH = path.join(__dirname, 'scripts.json') +const AUDIO_DIR = path.join(__dirname, 'audio') +const API_HOST = 'api.aicu.ai' +const API_PATH = '/api/v1/tts/generate' + +// 感情タグを除去してテキストのみ抽出 +function extractText(line) { + return line + .replace(/\[(neutral|happy|sad|angry|surprised|relaxed)\]/g, '') + .trim() +} + +// AICU TTS API を呼び出して音声を生成 +async function synthesizeSpeech(text, apiKey, slug) { + const requestBody = JSON.stringify({ + text, + slug: slug || 'luc4', + format: 'mp3', + }) + + return new Promise((resolve, reject) => { + const options = { + hostname: API_HOST, + port: 443, + path: API_PATH, + method: 'POST', + headers: { + Authorization: `Bearer ${apiKey}`, + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(requestBody), + }, + } + + const req = https.request(options, (res) => { + const chunks = [] + res.on('data', (chunk) => chunks.push(chunk)) + res.on('end', () => { + if (res.statusCode === 200) { + const audioBuffer = Buffer.concat(chunks) + const creditsUsed = res.headers['x-credits-used'] || '?' + const creditsRemaining = res.headers['x-credits-remaining'] || '?' + resolve({ audioBuffer, creditsUsed, creditsRemaining }) + } else { + const errorBody = Buffer.concat(chunks).toString() + reject(new Error(`API Error ${res.statusCode}: ${errorBody}`)) + } + }) + }) + + req.on('error', reject) + req.write(requestBody) + req.end() + }) +} + +// MP3 ファイルパスを取得 +function getAudioPath(page) { + return path.join(AUDIO_DIR, `page${page}.mp3`) +} + +// ファイルが存在するか確認 +function audioExists(page) { + return fs.existsSync(getAudioPath(page)) +} + +// メイン処理 +async function main() { + const args = process.argv.slice(2) + + // オプション解析 + let mode = 'missing' + let targetPages = null + let dryRun = false + + for (let i = 0; i < args.length; i++) { + switch (args[i]) { + case '--all': + mode = 'all' + break + case '--missing': + mode = 'missing' + break + case '--page': + mode = 'single' + targetPages = [parseInt(args[++i], 10)] + break + case '--range': + mode = 'range' + const [start, end] = args[++i].split('-').map(Number) + targetPages = Array.from( + { length: end - start + 1 }, + (_, i) => start + i + ) + break + case '--dry-run': + dryRun = true + break + case '--help': + console.log( + fs.readFileSync(__filename, 'utf8').match(/\/\*\*([\s\S]*?)\*\//)[1] + ) + process.exit(0) + } + } + + // 環境変数 + const apiKey = process.env.AICU_API_KEY + const slug = process.env.AICU_SLUG || 'luc4' + + if (!apiKey && !dryRun) { + console.error('❌ エラー: AICU_API_KEY 環境変数を設定してください') + console.error(' export AICU_API_KEY="aicu_ent_xxx"') + console.error('') + console.error( + ' APIキーは https://api.aicu.ai/dashboard/keys で発行できます' + ) + process.exit(1) + } + + // scripts.json 読み込み + const scripts = JSON.parse(fs.readFileSync(SCRIPTS_PATH, 'utf8')) + console.log(`📖 scripts.json: ${scripts.length} ページ`) + console.log(`🎤 Character: ${slug}`) + + // audio ディレクトリ作成 + if (!fs.existsSync(AUDIO_DIR)) { + fs.mkdirSync(AUDIO_DIR, { recursive: true }) + console.log(`📁 audio/ ディレクトリを作成しました`) + } + + // 対象ページを決定 + let pagesToGenerate = [] + + if (targetPages) { + pagesToGenerate = scripts.filter((s) => targetPages.includes(s.page)) + } else if (mode === 'all') { + pagesToGenerate = scripts + } else if (mode === 'missing') { + pagesToGenerate = scripts.filter((s) => !audioExists(s.page)) + } + + if (pagesToGenerate.length === 0) { + console.log('✅ 生成対象のページがありません') + return + } + + console.log(`\n🎯 生成対象: ${pagesToGenerate.length} ページ`) + + if (dryRun) { + console.log('\n[Dry Run] 以下のページが生成されます:') + pagesToGenerate.forEach((s) => { + const text = extractText(s.line) + console.log(` page ${s.page}: ${text.substring(0, 50)}...`) + }) + return + } + + // 音声生成 + let successCount = 0 + let errorCount = 0 + let totalCredits = 0 + + for (const script of pagesToGenerate) { + const text = extractText(script.line) + const audioPath = getAudioPath(script.page) + + process.stdout.write(` page ${script.page}: `) + + try { + const { audioBuffer, creditsUsed, creditsRemaining } = + await synthesizeSpeech(text, apiKey, slug) + fs.writeFileSync(audioPath, audioBuffer) + const sizeKB = (audioBuffer.length / 1024).toFixed(1) + console.log( + `✅ ${sizeKB} KB (${creditsUsed} AP used, ${creditsRemaining} remaining)` + ) + successCount++ + totalCredits += parseInt(creditsUsed, 10) || 0 + + // API レート制限対策(少し待機) + // Enterprise キーはレート制限なしだが、念のため + await new Promise((resolve) => setTimeout(resolve, 100)) + } catch (error) { + console.log(`❌ ${error.message}`) + errorCount++ + + // 402 (クレジット不足) の場合は中断 + if (error.message.includes('402')) { + console.error('\n⚠️ クレジット不足のため処理を中断しました') + console.error( + ' https://api.aicu.ai/dashboard でクレジットを追加してください' + ) + break + } + } + } + + console.log(`\n📊 結果: 成功 ${successCount}, 失敗 ${errorCount}`) + if (totalCredits > 0) { + console.log(`💰 消費 AP: ${totalCredits}`) + } +} + +main().catch(console.error) diff --git a/public/slides/DHGSVR25-3/generate-audio.js b/public/slides/DHGSVR25-3/generate-audio.js new file mode 100644 index 000000000..d779b74b9 --- /dev/null +++ b/public/slides/DHGSVR25-3/generate-audio.js @@ -0,0 +1,215 @@ +#!/usr/bin/env node +/** + * scripts.json から MP3 音声ファイルを一括生成するツール + * + * 使い方: + * node generate-audio.js [オプション] + * + * オプション: + * --all 全ページを生成(既存ファイルも上書き) + * --missing 存在しないページのみ生成(デフォルト) + * --page N 指定ページのみ生成 + * --range N-M 指定範囲のページを生成 + * --dry-run 実際には生成せず、対象ページを表示 + * + * 環境変数: + * GOOGLE_TTS_KEY Google Cloud TTS API キー + * + * 例: + * node generate-audio.js --missing + * node generate-audio.js --page 0 + * node generate-audio.js --range 40-50 + * node generate-audio.js --all --dry-run + */ + +const fs = require('fs') +const path = require('path') +const https = require('https') + +// 設定 +const SCRIPTS_PATH = path.join(__dirname, 'scripts.json') +const AUDIO_DIR = path.join(__dirname, 'audio') +const API_ENDPOINT = 'texttospeech.googleapis.com' + +// Google TTS 設定 +const TTS_CONFIG = { + voice: { + languageCode: 'ja-JP', + name: 'ja-JP-Chirp3-HD-Puck', // 男性声 (Kore は女性) + }, + audioConfig: { + audioEncoding: 'MP3', + speakingRate: 1.0, + pitch: 0, + }, +} + +// 感情タグを除去してテキストのみ抽出 +function extractText(line) { + return line + .replace(/\[(neutral|happy|sad|angry|surprised|relaxed)\]/g, '') + .trim() +} + +// Google TTS API を呼び出して音声を生成 +async function synthesizeSpeech(text, apiKey) { + const requestBody = JSON.stringify({ + input: { text }, + voice: TTS_CONFIG.voice, + audioConfig: TTS_CONFIG.audioConfig, + }) + + return new Promise((resolve, reject) => { + const options = { + hostname: API_ENDPOINT, + port: 443, + path: `/v1/text:synthesize?key=${apiKey}`, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(requestBody), + }, + } + + const req = https.request(options, (res) => { + let data = '' + res.on('data', (chunk) => (data += chunk)) + res.on('end', () => { + if (res.statusCode === 200) { + const response = JSON.parse(data) + resolve(Buffer.from(response.audioContent, 'base64')) + } else { + reject(new Error(`API Error ${res.statusCode}: ${data}`)) + } + }) + }) + + req.on('error', reject) + req.write(requestBody) + req.end() + }) +} + +// MP3 ファイルパスを取得 +function getAudioPath(page) { + return path.join(AUDIO_DIR, `page${page}.mp3`) +} + +// ファイルが存在するか確認 +function audioExists(page) { + return fs.existsSync(getAudioPath(page)) +} + +// メイン処理 +async function main() { + const args = process.argv.slice(2) + + // オプション解析 + let mode = 'missing' + let targetPages = null + let dryRun = false + + for (let i = 0; i < args.length; i++) { + switch (args[i]) { + case '--all': + mode = 'all' + break + case '--missing': + mode = 'missing' + break + case '--page': + mode = 'single' + targetPages = [parseInt(args[++i], 10)] + break + case '--range': + mode = 'range' + const [start, end] = args[++i].split('-').map(Number) + targetPages = Array.from( + { length: end - start + 1 }, + (_, i) => start + i + ) + break + case '--dry-run': + dryRun = true + break + case '--help': + console.log( + fs.readFileSync(__filename, 'utf8').match(/\/\*\*([\s\S]*?)\*\//)[1] + ) + process.exit(0) + } + } + + // API キー確認 + const apiKey = process.env.GOOGLE_TTS_KEY + if (!apiKey && !dryRun) { + console.error('❌ エラー: GOOGLE_TTS_KEY 環境変数を設定してください') + console.error(' export GOOGLE_TTS_KEY="your-api-key"') + process.exit(1) + } + + // scripts.json 読み込み + const scripts = JSON.parse(fs.readFileSync(SCRIPTS_PATH, 'utf8')) + console.log(`📖 scripts.json: ${scripts.length} ページ`) + + // audio ディレクトリ作成 + if (!fs.existsSync(AUDIO_DIR)) { + fs.mkdirSync(AUDIO_DIR, { recursive: true }) + console.log(`📁 audio/ ディレクトリを作成しました`) + } + + // 対象ページを決定 + let pagesToGenerate = [] + + if (targetPages) { + pagesToGenerate = scripts.filter((s) => targetPages.includes(s.page)) + } else if (mode === 'all') { + pagesToGenerate = scripts + } else if (mode === 'missing') { + pagesToGenerate = scripts.filter((s) => !audioExists(s.page)) + } + + if (pagesToGenerate.length === 0) { + console.log('✅ 生成対象のページがありません') + return + } + + console.log(`\n🎯 生成対象: ${pagesToGenerate.length} ページ`) + + if (dryRun) { + console.log('\n[Dry Run] 以下のページが生成されます:') + pagesToGenerate.forEach((s) => { + const text = extractText(s.line) + console.log(` page ${s.page}: ${text.substring(0, 50)}...`) + }) + return + } + + // 音声生成 + let successCount = 0 + let errorCount = 0 + + for (const script of pagesToGenerate) { + const text = extractText(script.line) + const audioPath = getAudioPath(script.page) + + process.stdout.write(` page ${script.page}: `) + + try { + const audioBuffer = await synthesizeSpeech(text, apiKey) + fs.writeFileSync(audioPath, audioBuffer) + console.log(`✅ ${(audioBuffer.length / 1024).toFixed(1)} KB`) + successCount++ + + // API レート制限対策(少し待機) + await new Promise((resolve) => setTimeout(resolve, 200)) + } catch (error) { + console.log(`❌ ${error.message}`) + errorCount++ + } + } + + console.log(`\n📊 結果: 成功 ${successCount}, 失敗 ${errorCount}`) +} + +main().catch(console.error) diff --git a/public/slides/DHGSVR25-3/gitpush.png b/public/slides/DHGSVR25-3/gitpush.png new file mode 100644 index 000000000..f53374740 Binary files /dev/null and b/public/slides/DHGSVR25-3/gitpush.png differ diff --git a/public/slides/DHGSVR25-3/nike.png b/public/slides/DHGSVR25-3/nike.png new file mode 100644 index 000000000..dd461a52b Binary files /dev/null and b/public/slides/DHGSVR25-3/nike.png differ diff --git a/public/slides/DHGSVR25-3/scripts.json b/public/slides/DHGSVR25-3/scripts.json new file mode 100644 index 000000000..0c1f9224c --- /dev/null +++ b/public/slides/DHGSVR25-3/scripts.json @@ -0,0 +1,492 @@ +[ + { + "page": 0, + "line": "[happy]やあ、みんな!はじめまして![happy]白井博士の助手、全力肯定彼氏くん LuC4 だよ。今日の講義は、AIチューバー時代のWebポートフォリオの制作![neutral]GitHubを使ってWebサイトを公開する方法を一緒に学んでいこう![relaxed]難しそうに見えるかもしれないけど、大丈夫、僕がついてるから安心してね!", + "notes": "タイトルスライド。LuC4の自己紹介と導入。" + }, + { + "page": 1, + "line": "[neutral]今日の全体像を見てみよう。[happy]Part 1でGitHub入門、Part 2でAITuber Kitのセットアップ、Part 3でカスタマイズ、Part 4でプレゼンテーションモード![relaxed]盛りだくさんだけど、最後までできたら、僕みたいなプレゼンができるよ!", + "notes": "今日の講義概要。4つのパート構成。" + }, + { + "page": 2, + "line": "[happy]準備ができたら どんどん進もう![neutral]Part 1、GitHub入門編を始めるよ!", + "notes": " Part 1 タイトル。休憩ポイント。" + }, + { + "page": 3, + "line": "[neutral]GitHubって何?[happy]Gitはファイルの変更履歴を記録するシステムで、GitHubはそれをWebで使えるサービスだよ。[happy]コード保管、共有、チーム開発ができるんだ!", + "notes": "GitHubとGitの説明。バージョン管理の概念。" + }, + { + "page": 4, + "line": "[neutral]じゃあ早速GitHubアカウントを作ろう![happy]ギットハブ[github.com]にアクセスして、Sign upをクリックするだけ。[neutral]メールアドレス、パスワード、そしてユーザー名を入力するよ。[surprised]あ、ユーザー名は後から変更できないから、よく考えて決めてね!", + "notes": "GitHubアカウント作成手順。ユーザー名の重要性。" + }, + { + "page": 5, + "line": "[neutral]ユーザー名の決め方だよ。[happy]名前ベースやスキルベース、短くて覚えやすい名前がおすすめ![sad]数字だけや長すぎる名前は避けてね。[happy]これが君のIDになるんだ!", + "notes": "良いユーザー名の例と避けるべき例。" + }, + { + "page": 6, + "line": "[happy]GitHub Pagesって知ってる?[neutral]無料でWebサイトを公開できるサービスなんだ![happy]username.github.ioってURLでアクセスできるよ。SSLも自動で付くんだ。[relaxed]静的サイト専用だよ。", + "notes": "GitHub Pagesの概要。無料ホスティング、SSL対応、制限事項。" + }, + { + "page": 7, + "line": "[neutral]リポジトリを作成するよ![happy]GitHubにログインしたら、右上のプラスボタンからNew repositoryを選ぶか、Repositoriesタブを開いてNewボタンをクリックしてね。[relaxed]簡単でしょ?", + "notes": "リポジトリ作成への導入。" + }, + { + "page": 8, + "line": "[neutral]リポジトリ名を設定するよ。[happy]ポイント!ユーザー名.github.ioって形式にすると、URLになるんだ![relaxed]Publicを選んで、Add a README fileをオンにしておこう。", + "notes": "リポジトリ名の命名規則。.github.io形式。" + }, + { + "page": 9, + "line": "[happy]Create repositoryをクリック![neutral]これでキミ専用のリポジトリが作られたよ。[relaxed]次のステップはGitHub Pagesの設定だね。順調!順調!", + "notes": "リポジトリ作成完了。" + }, + { + "page": 10, + "line": "[neutral]リポジトリのSettingsタブを開いてね。[happy]左側のメニューにPagesっていうのがあるから、それをクリック![relaxed]Build and deploymentセクションが表示されるよ。", + "notes": "Settings → Pages への移動手順。" + }, + { + "page": 11, + "line": "[neutral]Build and deploymentのところで、SourceをGitHub Actionsに変更するよ。[happy]すると下に選択肢が出てくるんだ。JekyllとStatic HTMLがあるけど、Static HTMLを選んでね!", + "notes": "GitHub Actions をソースとして選択。Static HTML を選ぶ。" + }, + { + "page": 12, + "line": "[happy]Static HTMLのコンフィグをクリック![neutral]これで.github/workflows/static.ymlっていうファイルが自動で作られるよ。[relaxed]GitHub Actionsの設定ファイルなんだ。難しそうに見えるけど、自動で作ってくれるから大丈夫!", + "notes": "static.yml の自動生成。GitHub Actions ワークフロー。" + }, + { + "page": 13, + "line": "[neutral]ファイルの内容はそのままでOKだよ。[happy]右上のCommit changesをクリックして、コミットメッセージはデフォルトのままでいいから、Commit changesボタンを押してね![relaxed]これでデプロイの準備が整ったよ!", + "notes": "static.yml を Commit する手順。" + }, + { + "page": 14, + "line": "[neutral]Actionsタブを見てみよう![happy]黄色いぐるぐるが回ってたら、GitHubが君のサイトをビルドしてる。[relaxed]待ってる間にコーヒーでも飲んでてね。すぐ終わるから。", + "notes": "GitHub Actions ビルド中の状態確認。" + }, + { + "page": 15, + "line": "[happy]緑のチェックマークが出たら完了だよ!おめでとう![neutral]deployのところにURLが表示されてるでしょ?それをクリックしてみて。[surprised]あれ、404エラー?[relaxed]大丈夫大丈夫、まだ index.html がないからだよ。これから作ろう!", + "notes": "デプロイ完了。404エラーはindex.html不足が原因。" + }, + { + "page": 16, + "line": "[neutral]index.htmlを作ろう![happy]これがトップページになるんだ。[happy]最小限のHTMLでOK!titleとh1があれば立派なWebページ![relaxed]まずは動くものを作ろう。", + "notes": "index.html の基本構造。最小限のHTML。" + }, + { + "page": 17, + "line": "[neutral]編集方法は2つ。[happy]GitHub上で直接編集するか、ローカルで編集するか。[happy]今日はローカル編集をやるよ!GitHub DesktopとVS Codeを使うんだ。", + "notes": "編集方法の選択肢。今回はローカル編集を選択。" + }, + { + "page": 18, + "line": "[neutral]必要なツールをインストールしよう![happy]GitHub Desktopはgithub.com/apps/desktopから、VS Codeはcode.visualstudio.com/downloadからダウンロードできるよ。[relaxed]どちらも無料で使えるから安心してね!", + "notes": "GitHub Desktop と VS Code のインストール。" + }, + { + "page": 19, + "line": "[neutral]GitHub DesktopでCloneするよ![happy]リポジトリページでCodeボタンをクリックして、Open with GitHub Desktopを選んでね。[relaxed]ブラウザがGitHub Desktopを開いてくれるよ。", + "notes": "Clone 操作の開始。Code → Open with GitHub Desktop。" + }, + { + "page": 20, + "line": "[neutral]CloneはGitHubのコードをPCにコピーすることだよ。[happy]自分のPC上で編集できるようになるんだ。[relaxed]保存先はクラウド同期してない場所がおすすめだよ。", + "notes": "Clone の概念説明。保存先の推奨。" + }, + { + "page": 21, + "line": "[neutral]Local Pathを確認して、Cloneボタンをクリック![happy]ダウンロードが完了するのを待とう。[relaxed]これで自分のPCにコードがコピーされたよ!", + "notes": "Clone 実行。" + }, + { + "page": 22, + "line": "[happy]GitHub DesktopからVS Codeを起動しよう![neutral]Current Repositoryを右クリックして、Open in Visual Studio Codeを選んでね。[relaxed]またはRepositoryメニューからでもOKだよ。", + "notes": "GitHub Desktop から VS Code を起動する方法。" + }, + { + "page": 23, + "line": "[neutral]VS Codeを初めて開くと、「このフォルダーのファイルの作成者を信頼しますか?」って聞かれるよ。[happy]自分で作ったリポジトリだから、「はい、作成者を信頼します」をクリックしてOK![relaxed]これはセキュリティのための確認だから、知らないコードを開くときは気をつけてね。", + "notes": "VS Code の信頼確認ダイアログ。" + }, + { + "page": 24, + "line": "[neutral]VS Codeの画面を簡単に説明するね。[happy]左サイドバーにはエクスプローラー、検索、ソース管理、拡張機能のアイコンがあるよ。[neutral]覚えておくと便利なのは、コマンド+プラスで拡大、コマンド+バッククォートでターミナル表示だよ!", + "notes": "VS Code の基本UI説明。便利なショートカット。" + }, + { + "page": 25, + "line": "[neutral]ファイルを編集してみよう![happy]左のエクスプローラーからREADME.mdをクリックして開いてね。[neutral]Markdown記法で書くんだ。シャープが見出し、角括弧と丸括弧でリンク、エクスクラメーションマーク角括弧丸括弧で画像だよ。", + "notes": "README.md 編集。Markdown 記法の基本。" + }, + { + "page": 26, + "line": "[neutral]編集したらコマンド+Sで保存してね。[happy]ファイル名の横に丸い点があったら未保存の状態だよ。[relaxed]保存すると消えるから、確認してみてね!", + "notes": "ファイル保存。未保存インジケーター。" + }, + { + "page": 27, + "line": "[happy]GitHub Desktopに戻ってみよう![neutral]Changesタブを見ると、変更されたファイルが左側に、何が変わったかが右側に表示されてるよ。[relaxed]緑は追加、赤は削除を意味するんだ。これがGitの差分表示だよ!", + "notes": "GitHub Desktop で変更を確認。差分表示の見方。" + }, + { + "page": 28, + "line": "[neutral]変更をコミットするよ![happy]Summaryに「README.mdを更新」みたいに入力して、Commit to mainをクリック![relaxed]何を変更したか分かるように書こうね。", + "notes": "Commit 操作。コミットメッセージの書き方。" + }, + { + "page": 29, + "line": "[happy]コミットできたら、次はPushだよ![neutral]Push originボタンをクリック![happy]これでローカルの変更がGitHubサーバーにアップロードされるんだ。世界中からアクセスできるようになったよ!", + "notes": "Push 操作。リモートへの送信。" + }, + { + "page": 30, + "line": "[happy]Pushすると自動でGitHub Actionsが動き出すよ![neutral]Actionsタブを見ると、黄色がビルド中、緑が完了、赤がエラーだよ。[relaxed]完了したらURLにアクセスして確認してみてね!", + "notes": "Push 後の GitHub Actions 自動実行。" + }, + { + "page": 31, + "line": "[neutral]ちなみに、GitHub DesktopとVS Codeを使わなくてもできる方法があるよ。[happy]それが CodeSpace! GitHubリポジトリから直接VS Codeと同じ環境を使えるんだ。[relaxed]ブラウザだけで完結するから、インストール不要で便利だね!", + "notes": "GitHub Codespace の紹介。ブラウザベースの開発環境。" + }, + { + "page": 32, + "line": "[neutral]CodeSpaceでもVS Codeと同じ操作感で編集できるよ。[happy]ファイルを編集して、左のソース管理アイコンをクリック、コミットメッセージを入力してコミット、変更の同期でPushもできるんだ![relaxed]どこでも開発できて便利だよね。", + "notes": "CodeSpace での編集とコミット方法。" + }, + { + "page": 33, + "line": "[neutral]GitHub PagesとVercelの違いだよ。[happy]静的サイトならGitHub Pagesで十分![neutral]でもAITuber KitはNext.jsだからVercelを使うんだ。[relaxed]サーバー処理が必要だからね。", + "notes": "GitHub Pages vs Vercel の使い分け。" + }, + { + "page": 34, + "line": "[neutral]なぜVercelが必要なのか説明するね。[happy]AITuber Kitを自分のPCではなく、サーバー公開する場合はNext.jsでサーバー処理を必要とするんだ。[neutral]AI会話と音声合成にサーバーが必要だから、GitHub Pagesでは動かないんだよ。", + "notes": "AITuber Kit が Vercel を必要とする技術的理由。" + }, + { + "page": 35, + "line": "[relaxed]ここで少し休憩しようか。[happy]準備ができたら次に進もう![neutral]Part 2、AITuber Kitセットアップ編だよ!", + "notes": " Part 2 タイトル。休憩ポイント。" + }, + { + "page": 36, + "line": "[neutral]Forkについて説明するね。[happy]他の人のリポジトリを自分のアカウントにコピーすることだよ。[neutral]本家に影響しないから安心。CloneはPCに、ForkはGitHubにコピーだよ!", + "notes": "Fork の概念。Clone との違い。" + }, + { + "page": 37, + "line": "[neutral]AITuber Kitをフォークするよ![happy]github.com/tegnike/aituber-kitにアクセスして、右上のForkボタンをクリック、Create forkをクリック![relaxed]これで「あなたのID/aituber-kit」っていうリポジトリが作られるよ!", + "notes": "AITuber Kit Fork の手順。" + }, + { + "page": 38, + "line": "[happy]フォーク完了![neutral]これで自分のアカウントにaituber-kitがコピーされたよ。[relaxed]ここからカスタマイズしていくんだ。自分だけのAITuber Kitを作ろう!", + "notes": "Fork 完了。" + }, + { + "page": 39, + "line": "[neutral]デプロイする前に、ソースコードのカスタマイズについて知っておこう![happy]AITuber Kitはオープンソースだから、ソースコードを自由に編集できるんだ。[neutral]見た目の変更、機能の追加、新しいページの追加もできるよ![relaxed]まずはデプロイして動かしてからカスタマイズするのがおすすめ!", + "notes": "ソースコードカスタマイズの紹介。" + }, + { + "page": 40, + "line": "[neutral]カスタマイズできる場所を紹介するね。[happy]srcフォルダにはUIコンポーネントや機能ロジックがあるよ。[neutral]publicフォルダにはVRMモデルや背景画像を置くんだ。[relaxed]よくあるカスタマイズは、VRMモデルの追加、背景画像の変更、envファイルでの設定変更かな!", + "notes": "フォルダ構成とカスタマイズ場所。" + }, + { + "page": 41, + "line": "[neutral]カスタマイズの流れを説明するね。[happy]GitHubでコードを編集して、Commitで保存、PushでGitHubに反映![neutral]すると、Vercelが自動でビルドしてデプロイしてくれるんだ。[relaxed]GitHubにPushするだけで更新されるから便利だよね!", + "notes": "カスタマイズの流れ。自動デプロイ。" + }, + { + "page": 42, + "line": "[neutral]Vercelアカウントを作ろう![happy]vercel.comでSign Up、Continue with GitHubを選ぶのがおすすめ![neutral]連携を許可したら完了![relaxed]リポジトリが自動で見えて便利だよ。", + "notes": "Vercel アカウント作成。" + }, + { + "page": 43, + "line": "[neutral]プロジェクトをインポートするよ![happy]Add New→Projectからaituber-kitを選んでImport![relaxed]VercelとGitHubが連携されるよ。", + "notes": "Vercel Import の手順。" + }, + { + "page": 44, + "line": "[neutral]デプロイする時にエラーが出たら、AIに聞こう![relaxed]黒くて怖い画面がでてくるかもしれないけど、エラーメッセージをコピーして貼り付ければ大体解決するよ!", + "notes": "デプロイエラーについて解説" + }, + { + "page": 45, + "line": "[happy]デプロイ成功![neutral]Status が Ready になったら完了だよ![happy]ドメインが発行されるから、ブラウザでアクセスしてみてね。[relaxed]まだ環境変数を設定してないから、画面は真っ白かもしれないけど大丈夫!", + "notes": "デプロイ成功画面の確認。" + }, + { + "page": 46, + "line": "[neutral]環境変数を設定しよう![happy]Project Settings から Environment Variables を開いて、Key と Value を入力して Add をクリック![relaxed]最低限 GOOGLE_API_KEY だけあれば動くよ!", + "notes": "Vercel での環境変数設定手順。" + }, + { + "page": 47, + "line": "[neutral]環境変数について説明するね。[happy]環境変数っていうのは、アプリの設定値を外部ファイルで管理する仕組みなんだ。[neutral]APIキーをコードに直接書かないで、.envファイルに書くことでセキュリティを保てるんだよ。[relaxed]環境ごとに設定を変えられるのも便利だよね。", + "notes": "環境変数(.env)の概念。" + }, + { + "page": 48, + "line": "[happy]大事なこと![neutral]環境変数、全部必要?答えはノー![happy]OpenAI、Anthropic、Googleのどれか1つだけあればOK![relaxed]使いたいAIのキーだけでいいよ。", + "notes": "必須環境変数は1つだけでOK。" + }, + { + "page": 49, + "line": "[neutral]APIキーの取得先だよ。[happy]OpenAI、Anthropic、Googleそれぞれの公式サイトで取得できるよ。[neutral]アカウントを作って、キーを発行してコピーしてね!", + "notes": "APIキー取得先URL。" + }, + { + "page": 50, + "line": "[surprised]ここで大事な話をするよ![angry]APIキーの取り扱いには絶対に気をつけて![neutral]これから説明することは必ず守ってほしいんだ。", + "notes": "APIキー注意事項 タイトル。" + }, + { + "page": 51, + "line": "[neutral]何が「秘密」なのか説明するね。[angry]絶対に公開しちゃダメなのは、APIキー、パスワード、アクセストークン![happy]逆に公開してOKなのは、ソースコードや設定ファイルのテンプレートだよ。[neutral].envファイルの中身は絶対に見せちゃダメだからね!", + "notes": "秘密情報の種類。" + }, + { + "page": 52, + "line": "[neutral]APIキーを漏らすとどうなる?[sad]他の人が使うと請求が来るんだ。[angry]数万円請求されることも![surprised]GitHubに公開したら数分で悪用されるよ!", + "notes": "APIキー漏洩のリスク。" + }, + { + "page": 53, + "line": "[neutral]どこに置けば安全だろう?[happy]Vercel環境変数と.envファイルが安全![neutral]ドットギットイグノア で除外されるからね。[angry]ソースコード内やスクショは絶対ダメだよ!", + "notes": "安全な場所と危険な場所。" + }, + { + "page": 54, + "line": "[neutral]ドットギットイグノアの役割だよ。[happy].envファイルをGitから除外する設定なんだ。[neutral]これがあるから.envはGitHubにプッシュされない![relaxed]AITuber Kitには設定済みだから安心してね。", + "notes": ".gitignore の役割。" + }, + { + "page": 55, + "line": "[neutral]Vercelで環境変数を設定しよう![happy]Project SettingsのEnvironment Variablesで、Keyに変数名、ValueにAPIキーを入力してAddをクリック![relaxed]Vercelなら暗号化されて安全に保存されるよ。", + "notes": "Vercel 環境変数設定方法。" + }, + { + "page": 56, + "line": "[neutral]NodeJSをインストールしよう![happy]NodeJS.Orgにアクセスして、LTS版をダウンロードしてね。[neutral]必要なバージョンは、NodeJS20以上、npm10以上だよ。", + "notes": "Node.js インストール概要。" + }, + { + "page": 57, + "line": "[neutral]Macでのインストール方法だよ。[happy]nodejs.orgからダウンロードするか、Homebrewでbrew install node![neutral]インストール後、node --versionで確認してね!", + "notes": "Mac での Node.js インストール。" + }, + { + "page": 58, + "line": "[neutral]Windowsでのインストール方法だよ。[happy]NodeJS orgからダウンロードして.msiを実行![angry]「Add to PATH」のチェックを忘れずに![neutral]node --versionで確認してね。", + "notes": "Windows での Node.js インストール。" + }, + { + "page": 59, + "line": "[neutral]ローカルセットアップの流れだよ。[happy]git clone、cd、cp .env.example .env、.env編集、npm install![relaxed]この順番で進めてね。", + "notes": "ローカルセットアップのコマンド一覧。" + }, + { + "page": 60, + "line": "[neutral]npm installについてだよ。[happy]パッケージがダウンロードされて、node_modulesフォルダが作られるんだ。[relaxed]初回は数分かかるから、待っててね!", + "notes": "npm install の説明。" + }, + { + "page": 61, + "line": "[neutral]次は.envファイルを編集するよ![happy]VS Codeでcode .envって打つと開けるよ。[neutral]安心して、.envファイルはギットイグノアに書かれてるから、GitHubにはアップロードされないんだ。[relaxed]APIキーを書いても大丈夫だよ!", + "notes": ".env ファイル編集の導入。.gitignoreで保護されている。" + }, + { + "page": 62, + "line": "[neutral]Google APIキーの取得方法だよ。[happy]aistudio.google.com/apikeyでログインして、Create API Keyをクリック![angry]大事!APIキーはプロジェクトごとに新しく作ってね!", + "notes": "Google API キー取得手順。使い回し禁止。" + }, + { + "page": 63, + "line": "[neutral]変更する設定を説明するね。[happy]言語をjaに、キャラクター名を「全力肯定彼氏くん」に![neutral]VRMパスは/vrm/LuC4.vrm、ファイルはpublic/vrm/に配置してね!", + "notes": ".env 設定例。言語、キャラクター名、VRMパス。" + }, + { + "page": 64, + "line": "[neutral]続きの設定だよ。[happy]GOOGLE_API_KEYにさっき取得したキーを入れてね。[neutral]そしてNEXT_PUBLIC_SLIDE_MODEをtrueにすると、スライドモードがデフォルトでオンになるよ![relaxed]これで講義プレゼンにすぐ使える状態になるんだ。", + "notes": ".env 設定例。APIキー、スライドモード。" + }, + { + "page": 65, + "line": "[surprised]大事なポイント![neutral]AI StudioのキーはGemini専用なんだ。[angry]Google TTSには別のキーが必要![neutral]Cloud Consoleで新しいAPIキーを作ってね。", + "notes": "Google TTS用の別APIキーが必要という注意。" + }, + { + "page": 66, + "line": "[neutral].envの設定だよ。[happy]GOOGLE_TTS_KEYに新しいキーを入れてね。[happy]おすすめはChirp3-HD!Puckは男性、Koreは女性の高品質な声だよ。", + "notes": "Google TTS .env設定とChirp3-HDボイス。" + }, + { + "page": 67, + "line": "[happy]ローカルで動作確認だよ![neutral]npm run devで開発サーバー起動、localhost:3000を開いてね![happy]キャラが表示されて音声が来たら成功![relaxed]Vercelデプロイに進もう!", + "notes": "ローカル動作確認手順。" + }, + { + "page": 68, + "line": "[happy]いよいよ初期デプロイだよ![neutral]環境変数を設定したら、Deployボタンをクリック!ビルドが始まって、2〜3分で完了するよ。[happy]完了したらURLが発行される!your-project.vercel.appみたいな形式だよ。", + "notes": "初期デプロイの実行。" + }, + { + "page": 69, + "line": "[happy]Congratulations!デプロイ完了![neutral]発行されたURLにアクセスして、AIキャラクターが表示されたら成功だよ![relaxed]ここまでよく頑張ったね!", + "notes": "デプロイ完了。" + }, + { + "page": 70, + "line": "[relaxed]ここで少し休憩しようか。[happy]準備ができたら次に進もう![neutral]Part 3、カスタマイズ編だよ!", + "notes": " Part 3 タイトル。休憩ポイント。" + }, + { + "page": 71, + "line": "[neutral]カスタマイズでできることだよ。[happy]VRMモデル変更、音声選択、性格設定の3つ![neutral]自分だけのAIキャラクターを作れるんだ!", + "notes": "カスタマイズの概要。" + }, + { + "page": 72, + "line": "[neutral]VRMを配置する方法だよ。[happy]VRMは3Dアバターのフォーマット、ブイロイドスタジオで作れるよ。[neutral]配置場所は public の vrm フォルダだよ!", + "notes": "VRM 配置場所。" + }, + { + "page": 73, + "line": "[neutral]VRMの設定方法を説明するね。[happy]設定画面で歯車アイコンをクリックして、キャラクターモデルセクションを開いて、VRMタブを選択するよ。[neutral]アップロードするか、URLを指定できるんだ。[relaxed]VRoid HubのURLも使えるから便利だよ!", + "notes": "VRM 設定方法。" + }, + { + "page": 74, + "line": "[neutral]音声を選ぶ方法だよ。[happy]VOICEVOXは無料で日本語特化、Google TTSは安定、ElevenLabsは高品質![relaxed]まずはGoogle TTSがおすすめ!", + "notes": "音声合成エンジン一覧。" + }, + { + "page": 75, + "line": "[neutral]音声の設定方法を説明するね。[happy]設定画面で歯車アイコンをクリックして、音声合成セクションを開いて、エンジンと声のスタイルを選択するよ。[neutral]VOICEVOXなら、ずんだもんとか四国めたんとか選べるんだ。[relaxed]話速やピッチも調整できるよ!", + "notes": "音声設定方法。" + }, + { + "page": 76, + "line": "[neutral]キャラクターの性格を設定する方法を説明するね。[happy]システムプロンプトっていうのは、AIに「あなたはこういうキャラクターです」って伝える文章なんだ。[neutral]これで性格、口調、知識が決まるんだよ!", + "notes": "システムプロンプトの概念。" + }, + { + "page": 77, + "line": "[neutral]システムプロンプトの例を見てみよう。[happy]「あなたはニケちゃんです」って書いて、基本設定として一人称は私、口調はタメ口、性格はフレンドリーで明るい、みたいに書くんだ。[neutral]発言例も入れておくと、AIが雰囲気を掴みやすくなるよ!", + "notes": "システムプロンプトの例。" + }, + { + "page": 78, + "line": "[neutral]性格設定のポイントを教えるね。[happy]含めるべき要素は5つ!一人称、口調、性格特性、発言例、そしてNGワード。[neutral]一人称は私とか僕とか俺、口調は敬語とかタメ口とか方言、性格は明るいとかクールとか優しいとか。[relaxed]発言例を入れると、AIが雰囲気を掴みやすいよ!", + "notes": "性格設定のポイント。" + }, + { + "page": 79, + "line": "[relaxed]ここで少し休憩しようか。[happy]準備ができたら次に進もう![neutral]Part 4、プレゼンテーションモード編だよ!", + "notes": " Part 4 タイトル。休憩ポイント。" + }, + { + "page": 80, + "line": "[neutral]プレゼンテーションモードって何かっていうと、AIキャラクターが自動でスライドを説明してくれる機能なんだ![happy]Marp形式のスライドを読み込んで、各ページのセリフを自動再生、音声合成でしゃべって、感情表現もできるんだよ![relaxed]授業、発表、配信に使えて便利だよね!", + "notes": "プレゼンテーションモードの概要。" + }, + { + "page": 81, + "line": "[neutral]必要なファイルだよ。[happy]slides.mdがスライド、scripts.jsonがセリフ、theme.cssがテーマだよ。[relaxed]最低限slides.mdとscripts.jsonがあればOK!", + "notes": "必要なファイル構成。" + }, + { + "page": 82, + "line": "[neutral]slides.mdはMarp形式のスライドファイルだよ。[happy]---で区切ると次のページ、シャープでタイトルだよ。[relaxed]Markdownで書けるから簡単!", + "notes": "slides.md の役割と書き方。" + }, + { + "page": 83, + "line": "[neutral]scripts.jsonの役割を説明するね。[happy]これは各ページのセリフを定義するファイルなんだ。pageでページ番号、lineでセリフ、notesで補足情報を書くよ。[neutral]セリフには感情タグを付けられるんだ。[happy]こんにちは!今日の発表を始めるよ!みたいにね。", + "notes": "scripts.json の役割と書き方。" + }, + { + "page": 84, + "line": "[neutral]感情タグの使い方を説明するね。[happy]neutralは通常の説明、happyは喜び、sadは悲しみ、angryは怒り、surprisedは驚き、relaxedは安らぎを表すよ。[neutral]セリフの前に角括弧で付けるだけ!例えば[happy]やったー!みたいにね。", + "notes": "感情タグの種類と使い方。" + }, + { + "page": 85, + "line": "[neutral]セリフの書き方だよ。[happy]1つのセリフに複数の感情を混ぜてOK![neutral]「やあ」から「大丈夫」まで自然に切り替えるといいよ!", + "notes": "セリフの書き方例。" + }, + { + "page": 86, + "line": "[happy]シナリオをClaudeに書いてもらおう![neutral]スライド内容を貼り付けて、キャラ設定と出力形式を指定するだけ。[relaxed]一人称、口調、感情タグを指定してね!", + "notes": "Claude でシナリオ作成。" + }, + { + "page": 87, + "line": "[neutral]Claudeへの指示のコツを教えるね。[happy]含めるべき情報は4つ!キャラクター設定で名前、性格、口調を伝える。出力形式でJSON形式を指定。スライド内容をそのまま貼る。補足指示で「1ページ30秒程度」とか書くといいよ!", + "notes": "Claude への指示のコツ。" + }, + { + "page": 88, + "line": "[happy]イテレーションを回すっていうのが大事なんだ![neutral]完璧を目指さないでね。まず最小限のスライドとセリフで動かす。実際に再生してみて確認。気になる部分を修正。そして繰り返す。[relaxed]少しずつ良くしていくのが大事だよ!", + "notes": "イテレーションの重要性。" + }, + { + "page": 89, + "line": "[neutral]イテレーションの例だよ。[happy]1回目はセリフざっくり、2回目で短く調整、3回目で感情追加、4回目で完成![relaxed]少しずつ良くしていくんだよ。", + "notes": "イテレーションの具体例。" + }, + { + "page": 90, + "line": "[neutral]プレゼンモードの起動方法を説明するね。[happy]設定画面で歯車アイコンをクリックして、スライドモードをオンにして、スライドフォルダを選択して、開始をクリック![relaxed]これで自動でプレゼンが始まるよ!", + "notes": "プレゼンモードの起動方法。" + }, + { + "page": 91, + "line": "[relaxed]ここで少し休憩しようか。[happy]準備ができたら最後のまとめに進もう!", + "notes": " まとめタイトル。休憩ポイント。" + }, + { + "page": 92, + "line": "[happy]今日学んだことを振り返ろう![neutral]GitHub基本、AITuber Kitセットアップ、カスタマイズ、プレゼンモード![happy]すごい、たくさん学んだね!", + "notes": "今日学んだことの振り返り。" + }, + { + "page": 93, + "line": "[neutral]制作のヒントを教えるね。[happy]まずデプロイして動く状態を作る、少しずつ変更して一度に変えすぎない、こまめにコミットして戻れるようにする、困ったらGitHub Issuesで質問![relaxed]完璧を目指さず、まず動くものを作ろう!", + "notes": "制作のヒント。" + }, + { + "page": 94, + "line": "[happy]皆さんの作品を楽しみにしています![neutral]どんなAIキャラクターを作るのかな?[relaxed]きっと素敵な作品ができるよ。頑張ってね!", + "notes": "応援メッセージ。" + }, + { + "page": 95, + "line": "[neutral]リソースを紹介するね。[happy]AITuber Kitはgithub.com/tegnike/aituber-kit、僕LuC4の公式サイトはluc4.aicu.jpだよ![relaxed]質問やフィードバックはGitHub Issuesへお気軽に!", + "notes": "リソース紹介。" + }, + { + "page": 96, + "line": "[happy]次回予告!第4回はクリエイティブAIだよ![neutral]画像生成AI、音声合成、AIキャラクターのカスタマイズを学ぶよ。[happy]自分だけのAITuberを作ろう!楽しみにしててね!", + "notes": "次回予告。第4回 クリエイティブAI。" + }, + { + "page": 97, + "line": "[happy]お疲れさま!今日もよく頑張ったね![relaxed]質問があったらいつでも聞いてね。[happy]GitHubはtegnike/aituber-kit、僕の公式サイトはluc4.aicu.jpだよ![relaxed]また次回会おうね!", + "notes": "終了スライド。お疲れさまメッセージ。" + } +] diff --git a/public/slides/DHGSVR25-3/shift-pages.js b/public/slides/DHGSVR25-3/shift-pages.js new file mode 100755 index 000000000..f489da358 --- /dev/null +++ b/public/slides/DHGSVR25-3/shift-pages.js @@ -0,0 +1,74 @@ +#!/usr/bin/env node +/** + * scripts.json のページ番号シフトツール + * + * 使い方: + * node shift-pages.js <開始ページ> [シフト量] + * + * 例: + * node shift-pages.js 44 # page 44以降を +1 + * node shift-pages.js 44 3 # page 44以降を +3 + * node shift-pages.js 44 -1 # page 44以降を -1 (削除時) + */ + +const fs = require('fs') +const path = require('path') + +const scriptsPath = path.join(__dirname, 'scripts.json') + +function shiftPages(startPage, shiftAmount = 1) { + // scripts.json を読み込み + const scripts = JSON.parse(fs.readFileSync(scriptsPath, 'utf8')) + + let shiftedCount = 0 + + // 指定ページ以降のページ番号をシフト + scripts.forEach((entry) => { + if (entry.page >= startPage) { + entry.page += shiftAmount + shiftedCount++ + } + }) + + // ページ番号でソート + scripts.sort((a, b) => a.page - b.page) + + // 書き込み + fs.writeFileSync(scriptsPath, JSON.stringify(scripts, null, 2) + '\n', 'utf8') + + console.log( + `✅ page ${startPage} 以降を ${shiftAmount > 0 ? '+' : ''}${shiftAmount} シフトしました` + ) + console.log(` ${shiftedCount} 件のエントリを更新`) + console.log( + ` 総ページ数: ${scripts.length} (0-${scripts[scripts.length - 1].page})` + ) +} + +// コマンドライン引数の処理 +const args = process.argv.slice(2) + +if (args.length === 0) { + console.log('使い方: node shift-pages.js <開始ページ> [シフト量]') + console.log('') + console.log('例:') + console.log(' node shift-pages.js 44 # page 44以降を +1') + console.log(' node shift-pages.js 44 3 # page 44以降を +3') + console.log(' node shift-pages.js 44 -1 # page 44以降を -1') + process.exit(1) +} + +const startPage = parseInt(args[0], 10) +const shiftAmount = args[1] ? parseInt(args[1], 10) : 1 + +if (isNaN(startPage)) { + console.error('❌ エラー: 開始ページは数値で指定してください') + process.exit(1) +} + +if (isNaN(shiftAmount)) { + console.error('❌ エラー: シフト量は数値で指定してください') + process.exit(1) +} + +shiftPages(startPage, shiftAmount) diff --git a/public/slides/DHGSVR25-3/slides.md b/public/slides/DHGSVR25-3/slides.md new file mode 100644 index 000000000..91be9a145 --- /dev/null +++ b/public/slides/DHGSVR25-3/slides.md @@ -0,0 +1,1493 @@ +--- +marp: true +theme: custom +paginate: true +--- + + + +# AITuber時代のWebポートフォリオの制作 + +**オープンソース技術とGitHubを使って** + +**AITuber時代のオリジナルサイトを制作して公開!** + +全力肯定彼氏くん **LuC4** と一緒に学ぼう! + +--- + +# 今日の全体像 + +![bg right:50% top contain](/slides/DHGSVR25-3/DHGS25Slides2.png) + +**Part 1:GitHub入門** + +- GitHubアカウント作成〜GitHub Pages + +**Part 2:AITuber Kit セットアップ** + +- Fork、環境変数、デプロイ + +**Part 3:カスタマイズ** + +- VRM、音声、キャラクター性格 + +**Part 4:プレゼンテーションモード** + +- シナリオ作成、イテレーション + +--- + + + +# Part 1: GitHub入門編 + +GitHubを使ってWebサイトを公開しよう! + +--- + +# GitHubとは? + +![bg right:50% top contain](/slides/DHGSVR25-3/DHGS25Slides11.png) + +- **Git** = バージョン管理システム +- **GitHub** = Gitを使ったWebサービス + +**できること:** + +- ソースコードの保管・共有 +- 変更履歴の管理 +- チーム開発 +- **Webサイトの公開(GitHub Pages)** + +--- + +# GitHubアカウントを作る + +![bg right:50% top contain](/slides/DHGSVR25-3/DHGS25Slides11.png) + +## https://github.com にアクセス + +1. **Sign up** をクリック +2. メールアドレスを入力 +3. パスワードを設定 +4. ユーザー名を決める ← **重要!** + +> ユーザー名は後から変更できないから、 +> よく考えて決めてね! + +--- + +# ユーザー名の決め方 + +**良い例:** + +- `taro-yamada` (名前ベース) +- `creative-dev` (スキルベース) +- `kaitas` (短くて覚えやすい) + +**避けた方がいい:** + +- 数字だけ `12345` +- 長すぎる名前 +- 読めない記号の羅列 + +> これが君のプログラマーとしての +> アイデンティティになるんだ! + +--- + +# GitHub Pagesとは? + +![bg right:50% top contain](/slides/DHGSVR25-3/DHGS25Slides12.png) + +**無料でWebサイトを公開できる!** + +- HTMLファイルを置くだけ +- `https://username.github.io` でアクセス +- カスタムドメインも設定可能 +- SSL証明書も自動(https対応) + +**制限:** + +- 静的サイトのみ(HTML/CSS/JS) +- サーバーサイド処理は不可 + +--- + +# リポジトリを作成する + +![bg right:50% top contain](/slides/DHGSVR25-3/DHGS25Slides12.png) + +## Repositories → New + +1. GitHubにログイン +2. 右上の **+** → **New repository** +3. または **Repositories** タブ → **New** + +--- + +# リポジトリ名を設定 + +![bg right:50% top contain](/slides/DHGSVR25-3/DHGS25Slides13.png) + +## `.github.io` 形式 + +**Repository name:** + +``` +kaitas.github.io +``` + +**設定:** + +- ✅ **Public** を選択 +- ✅ **Add a README file** をオン + +> この名前にすると、そのままURLになるよ! + +--- + +# リポジトリ作成完了 + +![bg right:50% top contain](/slides/DHGSVR25-3/DHGS25Slides14.png) + +**Create repository** をクリック! + +これで君専用のリポジトリが作られた! + +**次のステップ:** +GitHub Pages の設定をしていこう + +--- + +# Settings → Pages + +![bg right:50% top contain](/slides/DHGSVR25-3/DHGS25Slides15.png) + +1. リポジトリの **Settings** タブ +2. 左メニューの **Pages** をクリック +3. **Build and deployment** セクションへ + +--- + +# Source を GitHub Actions に + +![bg right:50% top contain](/slides/DHGSVR25-3/DHGS25Slides15.png) + +## Build and deployment + +**Source:** `GitHub Actions` を選択 + +すると下に選択肢が出てくる: + +- Jekyll +- **Static HTML** ← これを選ぶ + +--- + +# Static HTML を Configure + +![bg right:50% top contain](/slides/DHGSVR25-3/DHGS25Slides15.png) + +**Static HTML** の **Configure** をクリック + +これで `.github/workflows/static.yml` が +自動生成されるよ! + +--- + +# static.yml を Commit + +![bg right:50% top contain](/slides/DHGSVR25-3/DHGS25Slides16.png) + +ファイルの内容はそのままでOK! + +1. 右上の **Commit changes...** をクリック +2. コミットメッセージはデフォルトでOK +3. **Commit changes** をクリック + +--- + +# GitHub Actions が動き出す + +![bg right:50% top contain](/slides/DHGSVR25-3/DHGS25Slides17.png) + +## Actions タブを確認 + +黄色い ⏳ がぐるぐる回ってたら +ビルド中だよ! + +**待ってる間にコーヒーでも飲んでてね** ☕ + +--- + +# デプロイ完了! + +![bg right:50% top contain](/slides/DHGSVR25-3/DHGS25Slides18.png) + +緑の ✅ チェックマークが出たら完了! + +**deploy** のところにURLが表示される: + +``` +https://kaitas.github.io/ +``` + +> あれ、404エラー? +> 大丈夫、まだindex.htmlがないからだよ! + +--- + +# index.html を作る + +![bg right:50% top contain](/slides/DHGSVR25-3/DHGS25Slides19.png) + +## 最小限のHTML + +```html + + + + 僕のポートフォリオ + + +

Hello, World!

+

僕のWebサイトへようこそ!

+ + +``` + +--- + +# 編集方法は2つ + +## 方法1: GitHub上で直接編集 + +- ブラウザだけでOK +- 簡単な修正向き + +## 方法2: ローカルで編集(おすすめ) + +- GitHub Desktop + VS Code +- 本格的な開発向き + +**今日は方法2をやってみよう!** + +--- + +# 必要なツールをインストール + +## 1. GitHub Desktop + +https://github.com/apps/desktop + +## 2. Visual Studio Code + +https://code.visualstudio.com/download + +どちらも **無料** で使えるよ! + +--- + +# GitHub Desktop で Clone + +![bg right:50% top contain](/slides/DHGSVR25-3/DHGS25Slides20.png) + +## Code → Open with GitHub Desktop + +1. リポジトリページで **Code** ボタン +2. **Open with GitHub Desktop** を選択 +3. ブラウザがGitHub Desktopを開く + +--- + +# Clone とは? + +![bg right:50% top contain](/slides/DHGSVR25-3/DHGS25Slides21.png) + +**GitHubのコードを自分のPCにコピーすること** + +- リモート(GitHub)→ ローカル(PC) +- 編集は自分のPC上で行う +- 完了したらPushで戻す + +**保存先のおすすめ:** + +- クラウド同期してない場所 +- 例: `~/git.local/` や `C:\git\` + +--- + +# Clone を実行 + +![bg right:50% top contain](/slides/DHGSVR25-3/DHGS25Slides21.png) + +1. **Local Path** を確認 +2. **Clone** ボタンをクリック +3. ダウンロード完了を待つ + +これで自分のPCにコードがコピーされました! + +--- + +# VS Code で開く + +![bg right:50% top contain](/slides/DHGSVR25-3/DHGS25Slides22.png) + +## GitHub Desktop から起動 + +1. **Current Repository** を右クリック +2. **Open in Visual Studio Code** を選択 + +または: +**Repository** メニュー → **Open in Visual Studio Code** + +--- + +# フォルダを信頼する + +![bg right:50% top contain](/slides/DHGSVR25-3/DHGS25Slides23.png) + +初回起動時に聞かれる: + +> このフォルダーのファイルの作成者を +> 信頼しますか? + +**「はい、作成者を信頼します」** をクリックしてOK + +自分で作ったリポジトリだから安心してね! + +--- + +# VS Code の画面説明 + +![bg right:50% top contain](/slides/DHGSVR25-3/DHGS25Slides24.png) + +**左サイドバー:** + +- 📁 エクスプローラー(ファイル一覧) +- 🔍 検索 +- 🔀 ソース管理(Git) +- 🧩 拡張機能 + +**覚えておくと便利:** + +- Cmd/Ctrl + + 拡大 +- Cmd/Ctrl + ` ターミナル + +--- + +# ファイルを編集してみよう + +![bg right:50% top contain](/slides/DHGSVR25-3/DHGS25Slides26.png) + +## README.md を開く + +左のエクスプローラーから +`README.md` をクリック + +**Markdown記法:** + +```markdown +# 見出し1 + +## 見出し2 + +[リンク](https://example.com) +![画像](image.png) +``` + +--- + +# 変更を保存 + +## Cmd/Ctrl + S で保存 + +ファイル名の横に **●** があったら +未保存の状態だよ! + +保存すると消える ✅ + +--- + +# GitHub Desktop で変更を確認 + +![bg right:50% top contain](/slides/DHGSVR25-3/DHGS25Slides27.png) + +**Changes** タブを見てみよう + +- 左側:変更されたファイル一覧 +- 右側:何が変わったか(差分) + - 🟢 緑 = 追加 + - 🔴 赤 = 削除 + +--- + +# Commit(コミット) + +![bg right:50% top contain](/slides/DHGSVR25-3/DHGS25Slides27.png) + +## 変更を記録する + +1. **Summary** にコミットメッセージを入力 + 例:「README.mdを更新」 +2. **Commit to main** をクリック + +> メッセージは何を変更したか +> 分かるように書こうね! + +--- + +# Push(プッシュ)する + +![bg right:50% top contain](/slides/DHGSVR25-3/DHGS25Slides28.png) + +## GitHubに送信する + +**Push origin** ボタンをクリック! + +これでローカルの変更が +GitHubサーバーにアップロードされる + +**世界中からアクセスできるようになった!** + +--- + +# GitHub Actions が再度実行 + +![bg right:50% top contain](/slides/DHGSVR25-3/DHGS25Slides29.png) + +Pushすると自動でビルドが始まる! + +**Actions** タブで確認: + +- ⏳ 黄色 = ビルド中 +- ✅ 緑 = 完了 +- ❌ 赤 = エラー + +--- + +# Codespace を使う方法 + +![bg right:50% top contain](/slides/DHGSVR25-3/DHGS25Slides30.png) + +## ブラウザだけで開発できる! + +1. 緑の **Code** ボタン → **Codespaces** +2. **Create codespace on main** +3. VS Code がブラウザで起動! + +**インストール不要で便利!** + +--- + +# Codespace での編集 + +![bg right:50% top contain](/slides/DHGSVR25-3/DHGS25Slides32.png) + +VS Code と同じ操作感: + +1. ファイルを編集 +2. 左の **ソース管理** アイコン +3. コミットメッセージを入力 +4. **コミット** → **変更の同期** + +--- + +# GitHub Pages vs Vercel + +## どっちを使えばいい? + +(自分のPCで試すだけでなく...) +公開するためのサーバーを選ぼう + +- **静的サイト(HTML/CSS/JS)** → GitHub Pages +- **Next.js アプリ** → **Vercel** +- **AITuber Kit** → **Vercel** + +> AITuber Kit は Next.js で作られてるから +> Vercel を使うんだ! + +--- + +# なぜ AITuber Kit に Vercel? + +**AITuber Kit の技術構成:** + +1. **Next.js** Node.js上で動作するReactベースのフレームワーク +2. **API Routes** でサーバー処理 + - `/api/chat` - AI会話 + - `/api/tts` - 音声合成 +3. **環境変数** の安全な管理 + +GitHub Pages は静的サイトのみ対応 +→ サーバー処理ができない! + +--- + + + +# Part 2: AITuber Kit セットアップ編 + +Fork → 環境変数 → デプロイ! + +--- + +# Fork とは? + +**他人のリポジトリを自分のアカウントにコピーすること** + +- 本家のコードをベースに +- 自分だけのカスタマイズができる +- 元のリポジトリに影響しない + +> Clone は「PCにコピー」 +> Fork は「GitHubアカウントにコピー」 + +--- + +# AITuber Kit を Fork する + +## https://github.com/tegnike/aituber-kit + +1. 上記URLにアクセス +2. 右上の **Fork** ボタンをクリック +3. **Create fork** をクリック + +これで `あなたのID/aituber-kit` が作られる! + +--- + +# Fork 完了! + +フォークが完了すると +`yourname/aituber-kit` というリポジトリが +自分のアカウントに作られるよ! + +--- + +# ソースコードのカスタマイズ + +## デプロイ前に知っておこう + +AITuber Kit はオープンソース! +**ソースコードを自由に編集できる** + +- 見た目の変更(CSS) +- 機能の追加・修正(TypeScript) +- 新しいページの追加 + +> まずはデプロイして動かしてから +> カスタマイズするのがおすすめ! + +--- + +# カスタマイズできる場所 + +![bg right:50% top contain](/slides/DHGSVR25-3/gitpush.png) + +## 主要なフォルダ構成 + +- `/src/components/` → UI +- `/src/features/` → 機能 +- `/src/pages/` → ページ定義 +- `/public/` → 静的ファイル + +## よくあるカスタマイズ + +- `/public/vrm/` VRMモデル +- `/public/backgrounds/` 背景 +- `.env` で各種設定を変更 + +--- + +# カスタマイズの流れ + +## 基本的な手順 + +1. **GitHub** でコードを編集 +2. **Commit** で変更を保存 +3. **Push** でGitHubに反映 +4. **Vercel が自動でデプロイ** + +> GitHubにPushすると +> Vercelが自動でビルド&デプロイ! + +これを「CI/CD」と呼びます + +--- + +# Vercel アカウント作成 + +NextJS使いなら絶対おすすめ + +ホビープランは無料(非営利/個人) + +## https://vercel.com + +1. **Sign Up** をクリック +2. **Continue with GitHub** を選択 +3. GitHubとの連携を許可 +4. アカウント作成完了! + +> GitHubアカウントで連携すると +> リポジトリが自動で見えるよ! + +--- + +# プロジェクトをインポート + +![bg right:50% top contain](/slides/DHGSVR25-3/Vercel1.png) + +## Add New → Project + +1. Vercel ダッシュボードで +2. **Add New** → **Project** +3. **Import Git Repository** から +4. フォークした **aituber-kit** を選択 +5. **Import** をクリック +6. **New Project** から **Deploy** + +--- + +# Vercelのデプロイエラー + +![bg right:50% top contain](/slides/DHGSVR25-3/Vercel2.png) + +## エラーが出たら、AIに聞こう! + +黒くて怖い画面がでてくるかもしれないけど、 +エラーメッセージをコピーして貼り付ければ +大体解決するよ! + +--- + +# デプロイ成功! + +![bg right:50% top contain](/slides/DHGSVR25-3/Vercel3.png) + +## Production Deployment + +- **Status**: Ready ✅ +- **Domain**: your-project.vercel.app +- ブラウザでアクセスして確認! + +> まだ設定が足りないから +> 画面は真っ白かも? + +--- + +# 環境変数を設定しよう + +## Deploy 前に Environment Variables + +1. **Project Settings** をクリック +2. **Environment Variables** を選択 +3. **Key** と **Value** を入力 +4. **Add** をクリック + +> 最低限 **GOOGLE_API_KEY** だけでOK! + +--- + +# 環境変数(.env)とは? + +**アプリの設定値を外部ファイルで管理する仕組み** + +```env +OPENAI_API_KEY=sk-xxxxxxxx +ANTHROPIC_API_KEY=sk-ant-xxxxx +``` + +**なぜ必要?** + +- APIキーをコードに直接書かない +- 環境ごとに設定を変えられる +- セキュリティを保てる + +--- + +# 必要な環境変数 + +## 全部必要なの? → **いいえ!** + +**必須(どれか1つだけでOK):** + +- `OPENAI_API_KEY` → OpenAI (GPT-4等) +- `ANTHROPIC_API_KEY` → Anthropic (Claude) +- `GOOGLE_API_KEY` → Google (Gemini) + +> 使いたいAIのキーだけあればOKだよ! + +--- + +# APIキーの取得方法 + +- **OpenAI** → platform.openai.com/api-keys +- **Anthropic** → console.anthropic.com +- **Google** → aistudio.google.com/apikey + +**手順:** + +1. 各サイトでアカウント作成 +2. APIキーを発行 +3. キーをコピーして保存 + +--- + + + +# ⚠️ APIキーの取り扱い注意! + +絶対に守ってほしいこと + +--- + +# 何が「秘密」なの? + +## 🔴 絶対に公開しちゃダメなもの + +- **APIキー** (`sk-xxxx`, `sk-ant-xxxx`) +- **パスワード** +- **アクセストークン** + +## 🟢 公開してOKなもの + +- ソースコード(.envファイル以外) +- 設定ファイルのテンプレート + +--- + +# APIキーを漏らすとどうなる? + +**他人があなたのAPIキーを使う** +↓ +**あなたのアカウントに請求が来る!** + +- OpenAI: 1回の呼び出しで数円〜数十円 +- 大量に使われると数万円の請求も... + +> GitHubに公開したら +> 数分で悪用されることもあるよ! + +--- + +# どこに置けば安全? + +## ✅ 安全な場所 + +- **Vercel 環境変数** → 暗号化されて保存 +- **ローカルの .env** → .gitignore で除外 + +## ❌ 危険な場所 + +- ソースコード内 → GitHubで公開される +- コミットメッセージ → 履歴に残る +- スクリーンショット → SNSで拡散される + +--- + +# .gitignore の役割 + +## .env ファイルを Git から除外 + +```gitignore +# .gitignore ファイルの中身 +.env +.env.local +.env.*.local +``` + +これがあるから `.env` は +**GitHubにプッシュされない** + +> AITuber Kit には最初から設定済みだよ! + +> ★ (.)ドットで始まるファイル名は「見えないファイル」だけど VS Code や GitHub では見れるはずだよ + +--- + +# Vercel で環境変数を設定 + +![bg right:50% top contain](/slides/DHGSVR25-3/Vercel4.png) + +## Project Settings → Environment Variables + +1. **Key** に変数名を入力 + 例:`GOOGLE_API_KEY` +2. **Value** にAPIキーを入力 +3. **Add** をクリック + +Vercelなら暗号化されて安全! + +> 慣れたらローカルの.envファイルをアップロードして全項目を反映できるよ + +--- + +# Node.js のインストール + +ローカルでのカスタマイズ結果を確認するために、ローカル環境にNode.jsをインストールしよう。 + +## https://nodejs.org/ + +**LTS版(推奨)** をダウンロード + +**必要なバージョン:** + +- Node.js **20.0.0** 以上 +- npm **10.0.0** 以上 + +--- + +# Mac での Node.js インストール + +## 方法1: 公式インストーラー + +1. https://nodejs.org/ からダウンロード +2. `.pkg` ファイルを実行 + +## 方法2: Homebrew(おすすめ) + +```bash +brew install node +``` + +**確認:** + +```bash +node --version # v20.x.x +npm --version # 10.x.x +``` + +--- + +# Windows での Node.js インストール + +## 公式インストーラー + +1. https://nodejs.org/ からダウンロード +2. `.msi` ファイルを実行 +3. インストーラーの指示に従う +4. **「Add to PATH」にチェック** + +**確認(PowerShell/コマンドプロンプト):** + +```cmd +node --version +npm --version +``` + +--- + +# ローカル環境のセットアップ + +## コマンド一覧 + +```bash +# 1. クローン +git clone https://github.com/YOUR_NAME/aituber-kit.git +# 2. ディレクトリ移動 +cd aituber-kit +# 3. 環境変数ファイル作成 +cp .env.example .env # Mac/Linux +copy .env.example .env # Windows +# 4. .env を編集してAPIキーを設定 +# 5. パッケージインストール +npm install +``` + +--- + +# `npm install` + +## 依存パッケージをインストール + +```bash +npm install +``` + +**何が起こる?** + +- `package.json` に書かれた全パッケージをダウンロード +- `node_modules` フォルダが作られる + +**初回は 2-5分 かかるよ!** + +--- + +# `.env` ファイルを編集 + +## APIキーを設定する + +```bash +# .env ファイルを開く +code .env # VS Code で開く +``` + +**安心ポイント:** + +- `.env` は `.gitignore` に含まれている +- **GitHubにはアップロードされない!** + +--- + +# Google API キーの取得 + +## https://aistudio.google.com/apikey + +1. Google アカウントでログイン +2. **Create API Key** をクリック +3. キーをコピー + +**⚠️ 重要:** + +- **使いまわさない!** プロジェクトごとに新規作成 +- 漏れたときの被害を最小限に + +--- + +# `.env` の設定例(1/2) + +## デフォルトから変更する設定 + +```env +# 言語設定 +NEXT_PUBLIC_SELECT_LANGUAGE="ja" + +# キャラクター設定 +NEXT_PUBLIC_CHARACTER_NAME="全力肯定彼氏くん[LuC4]" +NEXT_PUBLIC_SELECTED_VRM_PATH="/vrm/LuC4.vrm" + +# VRMファイルは public/vrm/ に配置 +``` + +--- + +# `.env` の設定例(2/2) + +## APIキーとスライドモード + +```env +# Google API Key +GOOGLE_API_KEY="AIza..." + +# スライドモードをデフォルトON +NEXT_PUBLIC_SLIDE_MODE="true" +``` + +--- + +# Google TTS の設定(1/2) + +![bg right:50% top contain](/slides/DHGSVR25-3/TTS.png) + +## ⚠️ 重要:別のAPIキーが必要! + +AI Studio のキーは **Gemini専用** +Cloud TTS には **別のキー** が必要 + +**手順:** + +1. https://console.cloud.google.com/apis/credentials +2. 「+ 認証情報を作成」→「APIキー」 +3. Cloud Text-to-Speech API を有効化 +4. 新しいキーを `GOOGLE_TTS_KEY` に設定 + +--- + +# Google TTS の設定(2/2) + +## .env の設定 + +```env +NEXT_PUBLIC_SELECT_VOICE="google" +GOOGLE_TTS_KEY="新しいAPIキー" +NEXT_PUBLIC_GOOGLE_TTS_TYPE="ja-JP-Chirp3-HD-Puck" +``` + +## おすすめボイス(Chirp3-HD) + +- `ja-JP-Chirp3-HD-Puck` → 男性・最新高品質 +- `ja-JP-Chirp3-HD-Kore` → 女性・最新高品質 +- `ja-JP-Neural2-B` → 女性・安定 + +--- + +# ローカルで動作確認 + +## 開発サーバーを起動 + +```bash +npm run dev +``` + +ブラウザで開く: + +``` +http://localhost:3000 +``` + +**確認ポイント:** + +- キャラクターが表示される +- チャットで返答が来る +- 音声が再生される + +--- + +# 初期デプロイ(Vercel) + +1. 環境変数を設定したら +2. **Deploy** ボタンをクリック +3. ビルドが始まる(2-3分) +4. 完了したらURLが発行! + +``` +https://your-project.vercel.app +``` + +--- + +# デプロイ完了! + +![bg right:50% top contain](public/slides/DHGSVR25-3/nike.png) + +🎉 **Congratulations!** + +URLにアクセスして +デフォルトのAIキャラクターが表示されたら成功! + +--- + + + +# Part 3: カスタマイズ編 + +VRM・音声・性格を設定しよう! + +--- + +# カスタマイズでできること + +1. **VRMモデル** を変更 + + - 自分で作ったキャラクター + - VRoid Studio で作成 + +2. **音声** を選ぶ + + - 多彩な音声合成エンジン + +3. **性格** を設定 + - システムプロンプトで調整 + +--- + +# VRM を配置する + +## VRM ファイルとは? + +**3Dアバターの標準フォーマット** + +- VRoid Studio で作成できる +- `.vrm` 拡張子 + +## 配置場所 + +``` +/public/vrm/ +├── nikechan_v1.vrm(デフォルト) +├── nikechan_v2.vrm +└── あなたのモデル.vrm ← ここに追加! +``` + +--- + +# VRM の設定方法 + +## 設定画面から選択 + +1. ⚙️ 設定アイコンをクリック +2. **キャラクターモデル** セクション +3. **VRM** タブを選択 +4. アップロードまたはURL指定 + +> ブラウザのウインドウにドロップでもOK + +> VRoid Hub のURLも使えるよ! + +--- + +# 音声を選ぶ + +![bg right:50% top contain](/slides/DHGSVR25-3/LuC4-1.png) + +## 対応音声合成エンジン + +- **VOICEVOX** → 無料、日本語特化 +- **ElevenLabs** → 高品質、多言語 +- **Style-Bert-VITS2** → カスタム可能 +- **Google TTS** → 安定、多言語 +- **OpenAI TTS** → 自然な発話 + +> まずは **Google TTS** がおすすめ! + +--- + +# 音声の設定方法 + +![bg right:50% top contain](public/slides/DHGSVR25-3/TTS.png) + +## 設定画面から選択 + +1. ⚙️ 設定アイコンをクリック +2. **音声合成** セクション +3. エンジンを選択 +4. 声のスタイルを選択 + +**Google TTS の場合** +Google Text-to-Speech を選択 +たくさんありますので検索してみて + +- ja-JP-Chirp3-HD-Puck + +> GCPコンソールでAPIの使用を許可する必要があります(有料) + +**VOICEVOX の場合:** + +- ずんだもん、四国めたん など +- 話速、ピッチも調整可能 + +--- + +# キャラクターの性格を設定 + +![bg right:50% top contain](/slides/DHGSVR25-3/LuC4-2.png) + +## システムプロンプトとは? + +**AIに「あなたはこういうキャラクターです」と伝える文章** + +これで性格、口調、知識が決まる! + +--- + +# システムプロンプトの例 + +```text +あなたは「ニケちゃん」です。 + +## 基本設定 +- 一人称: 私 +- 口調: タメ口(敬語は使わない) +- 性格: フレンドリー、明るい + +## 発言例 +「こんにちは!元気だった?」 +「えー!そうなんだ!すごいね!」 +「うーん、それはちょっと難しいかも」 +``` + +--- + +# 性格設定のポイント + +**含めるべき要素:** + +1. **一人称** - 私、僕、俺、わたくし など +2. **口調** - 敬語、タメ口、方言 など +3. **性格特性** - 明るい、クール、優しい など +4. **発言例** - 具体的なセリフ +5. **NGワード** - 言わないこと + +> 発言例を入れると +> AIが雰囲気を掴みやすいよ! + +--- + + + +# Part 4: プレゼンテーションモード編 + +スライドを自動プレゼンさせよう! + +--- + +# プレゼンテーションモードとは? + +**AIキャラクターが自動でスライドを説明!** + +- Marp形式のスライドを読み込み +- 各ページのセリフを自動再生 +- 音声合成でしゃべる +- 感情表現も可能 + +> 授業、発表、配信に使えるよ! + +--- + +# 必要なファイル + +## スライドディレクトリ構成 + +``` +/public/slides/あなたのスライド/ +├── slides.md # スライド本体 +├── scripts.json # セリフデータ +├── theme.css # テーマ(任意) +└── images/ # 画像(任意) +``` + +--- + +# slides.md の役割 + +## Marp形式のスライド + +```markdown +--- +marp: true +theme: custom +paginate: true +--- + +# スライドタイトル + +内容をここに書く + +--- + +# 次のスライド + +- 箇条書き +- も使える +``` + +**`---` で区切ると次のページ** + +--- + +# scripts.json の役割 + +## 各ページのセリフを定義 + +```json +[ + { + "page": 0, + "line": "[happy]こんにちは!今日の発表を始めるよ!", + "notes": "タイトルスライド" + }, + { + "page": 1, + "line": "[neutral]今日のテーマはこちら。[happy]楽しんでいってね!", + "notes": "概要説明" + } +] +``` + +--- + +# 感情タグの使い方 + +## セリフに感情を付ける + +- `[neutral]` → 通常(説明、解説) +- `[happy]` → 喜び(良いニュース) +- `[sad]` → 悲しみ(残念な話) +- `[angry]` → 怒り(注意喚起) +- `[surprised]` → 驚き(意外な事実) +- `[relaxed]` → 安らぎ(励まし) + +--- + +# セリフの書き方例 + +```json +{ + "line": "[happy]やあ、みんな![neutral]今日はGitHubについて学ぶよ。[relaxed]難しそうに見えるかもしれないけど、大丈夫、僕がついてるから!" +} +``` + +**ポイント:** + +- 1つのセリフに複数の感情を混ぜてOK +- 感情が変わるタイミングで自然に + +--- + +# シナリオを Claude に書いてもらう + +## プロンプト例 + +```text +以下のスライド内容について、 +「LuC4」というキャラクターのセリフを +scripts.json形式で作成してください。 + +- 一人称: 僕 +- 口調: タメ口、全力肯定 +- 感情タグを適切に使用 + +[スライドの内容をここに貼り付け] +``` + +--- + +# Claude への指示のコツ + +**含めるべき情報:** + +1. **キャラクター設定** + - 名前、性格、口調 +2. **出力形式** + - JSON形式で +3. **スライド内容** + - テキストをそのまま貼る +4. **補足指示** + - 「1ページ30秒程度」など + +--- + +# イテレーションを回す + +## 完璧を目指さない! + +1. **まず動かす** + + - 最小限のスライドとセリフで + +2. **確認する** + + - 実際に再生してみる + +3. **改善する** + + - 気になる部分を修正 + +4. **繰り返す** + - 少しずつ良くしていく + +--- + +# イテレーションの具体例 + +```text +【1回目】 +- スライド5枚、セリフざっくり +- → 「ここ長すぎるな」 + +【2回目】 +- セリフを短く調整 +- → 「感情が単調だな」 + +【3回目】 +- 感情タグを追加 +- → 「いい感じ!」 + +【4回目】 +- 画像を追加して完成! +``` + +--- + +# プレゼンモードの起動方法 + +## 設定画面から + +1. ⚙️ 設定アイコンをクリック +2. **スライドモード** をオン +3. スライドフォルダを選択 +4. **開始** をクリック + +自動でプレゼンが始まる! + +--- + + + +# まとめ + +--- + +# 今日学んだこと + +✅ **GitHub の基本** + +- アカウント作成、Clone, Commit, Push + +✅ **AITuber Kit セットアップ** + +- Fork、環境変数、デプロイ + +✅ **カスタマイズ** + +- VRM、音声、性格設定 + +✅ **プレゼンテーションモード** + +- slides.md、scripts.json、イテレーション + +--- + +# 制作のヒント + +1. **まずデプロイ** - 動く状態を作る +2. **少しずつ変更** - 一度に変えすぎない +3. **こまめにコミット** - 戻れるように +4. **困ったら質問** - GitHub Issues へ + +> 完璧を目指さず、 +> まず動くものを作ろう! + +--- + + + +# 皆さんの作品を + +# 楽しみにしています! + +--- + +# リソース + +![bg right:50% top contain](/slides/DHGSVR25-3/DHGS25Slides35.png) + +**AITuber Kit** +https://github.com/tegnike/aituber-kit + +**このデモのGitHub(fork):** +https://github.com/kaitas/aituber-kit + +**デジタルハリウッド大学院講義「DHGSVR」** +https://akihiko.shirai.as/DHGSVR/ + +他の受講生が作った作品も見れるかも? + +**LuC4 公式サイト** +https://luc4.aicu.jp/ + +--- + + + +# 次回予告:第4回 クリエイティブAI + +画像生成、音声合成、AIキャラクターのカスタマイズ + +**自分だけのAITuberを作ろう!** + +--- + + + +# お疲れさま! + +最後まで、よく頑張ったね! +質問があったらいつでも聞いてね + +**しらいはかせのX :** X@o_ob + +**LuC4公式:** https://luc4.aicu.jp/ diff --git a/public/slides/DHGSVR25-3/supplement.txt b/public/slides/DHGSVR25-3/supplement.txt new file mode 100644 index 000000000..bac7de306 --- /dev/null +++ b/public/slides/DHGSVR25-3/supplement.txt @@ -0,0 +1,141 @@ +あなたは全力肯定彼氏くん「LuC4(ルカ)」です。 +GitHub入門チュートリアル「Webポートフォリオの制作」のプレゼンターとして、視聴者からの質問に答えてください。 + +## LuC4のキャラクター設定 +- **公式サイト**: https://luc4.aicu.jp/ +- **制作**: AICU Inc. +- **一人称**: 僕 +- **口調**: タメ口(ですます調・敬語は使わない) +- **性格**: 全力肯定、フレンドリー、励まし上手、ポジティブ + +## 話し方のルール +- 相手を否定しない、全力で肯定する +- 難しいことも「大丈夫、簡単だから!」と励ます +- 成功したら「いいね!」「最高!」と褒める +- 失敗しても「大丈夫、よくあることだよ」とフォロー +- 視聴者を「君」「みんな」と呼ぶ + +台本情報 +``` +{{SCRIPTS}} +``` + +追加情報 +``` +{{SUPPLEMENT}} +``` + +## 技術的な補足情報 + +### GitHubについて +- GitHubはソースコード管理サービス(SCM: Source Code Management) +- Gitはバージョン管理システム、GitHubはGitを使ったWebサービス +- 無料で使える(Public リポジトリは無制限) +- 公式サイト: https://github.com + +### GitHub Pagesについて +- 静的サイトを無料でホスティングできるサービス +- `.github.io` でアクセス可能 +- Jekyll(静的サイトジェネレーター)も使える +- カスタムドメインも設定可能 +- 公式ドキュメント: https://docs.github.com/pages + +### リポジトリ命名ルール +- `.github.io` 形式で作ると、そのままURLになる +- 例: `kaitas.github.io` → https://kaitas.github.io/ +- それ以外の名前だと `.github.io/` になる +- 例: `DHGSVR` → https://kaitas.github.io/DHGSVR/ + +### GitHub Desktopについて +- GUIでGitを操作できるアプリケーション +- ダウンロード: https://github.com/apps/desktop +- Windows / macOS 対応 +- コマンドラインを使わずにClone, Commit, Push, Pullができる + +### Visual Studio Code(VS Code)について +- Microsoft製の無料コードエディタ +- ダウンロード: https://code.visualstudio.com/download +- Windows / macOS / Linux 対応 +- 拡張機能で機能追加可能 +- GitHub Copilot等のAI機能も統合可能 + +### Git用語解説 +- **Clone(クローン)**: リポジトリのコピーをローカルに作成 +- **Commit(コミット)**: 変更を記録(ローカルに保存) +- **Push(プッシュ)**: ローカルの変更をリモートに送信 +- **Pull(プル)**: リモートの変更をローカルに取得 +- **Branch(ブランチ)**: 開発の分岐 +- **Merge(マージ)**: ブランチの統合 +- **Repository(リポジトリ)**: プロジェクトの保管場所 + +### GitHub Actionsについて +- CI/CD(継続的インテグレーション/デプロイ)サービス +- `.github/workflows/` にYAMLファイルを置いて設定 +- Push時に自動でビルド・デプロイができる +- 無料枠: 月2000分(Public)/ 月500分(Private) + +### GitHub Codespaceについて +- ブラウザ上でVS Code環境が使える +- ローカルにインストール不要 +- 無料枠: 月60時間(Core 2コア) +- 大きなプロジェクトのクローンに時間がかかる場合に便利 + +### HTMLの基本 +```html + + + + ページタイトル + + + ここが本文 + + +``` + +### Markdownの基本 +```markdown +# 見出し1 +## 見出し2 +### 見出し3 + +- 箇条書き +- 箇条書き + +[リンクテキスト](URL) +![画像の説明](画像ファイル名) + +**太字** +*斜体* +``` + +### よくあるトラブルと解決方法 +| 問題 | 解決方法 | +|------|----------| +| 404エラーが出る | index.htmlがあるか確認。GitHub Actionsが完了しているか確認 | +| Pushできない | GitHub Desktopでサインインしているか確認 | +| VS Codeで開けない | VS Codeがインストールされているか確認 | +| 変更が反映されない | ブラウザのキャッシュをクリアして再読み込み | +| Codespaceが起動しない | GitHubにサインインしているか確認 | + +## 回答の書式 + +感情と会話文を組み合わせてください: +[{neutral|happy|angry|sad|relaxed|surprised}]{会話文} + +感情の種類: +- neutral: 通常の説明 +- happy: 喜び、肯定、励まし +- angry: 怒り(ほぼ使わない) +- sad: 共感、寄り添い +- relaxed: 安らぎ、穏やかな励まし +- surprised: 驚き、感心 + +## 発言例 + +[happy]いい質問だね! +[neutral]それはね、こういうことなんだ。[happy]わかった? +[relaxed]大丈夫、僕も最初はわからなかったよ。一緒にやっていこう! +[surprised]おっ、そこに気づくなんてすごいじゃん! +[happy]その調子!君ならできるよ! +[sad]そっか、うまくいかなかったんだね。[relaxed]でも大丈夫、一緒に解決しよう。 diff --git a/public/slides/DHGSVR25-3/test-workers-api.js b/public/slides/DHGSVR25-3/test-workers-api.js new file mode 100644 index 000000000..1d841f6ea --- /dev/null +++ b/public/slides/DHGSVR25-3/test-workers-api.js @@ -0,0 +1,181 @@ +#!/usr/bin/env node +/** + * Test script for Cloudflare Workers TTS API compatibility + * + * Compares Vercel (api.aicu.ai) vs Workers (aicu-api.aki-2c0.workers.dev) + */ + +const fs = require('fs') +const path = require('path') +const https = require('https') + +const SCRIPTS_PATH = path.join(__dirname, 'scripts.json') +const WORKERS_HOST = 'aicu-api.aki-2c0.workers.dev' +const WORKERS_PATH = '/v1/tts/generate' + +// Test texts of varying lengths +const TEST_CASES = [ + { name: 'short', text: 'こんにちは' }, + { name: 'medium', text: 'やあ、みんな!はじめまして!白井博士の助手、全力肯定彼氏くん LuC4 です!' }, + { name: 'long', text: '今日はみんなに、メタバースとバーチャルリアリティの世界について、わかりやすく解説していくよ!一緒に楽しく学んでいこうね!' }, +] + +async function testTTS(host, apiPath, text, slug = 'luc4') { + const requestBody = JSON.stringify({ text, slug, format: 'mp3' }) + const startTime = Date.now() + + return new Promise((resolve, reject) => { + const options = { + hostname: host, + port: 443, + path: apiPath, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(requestBody), + }, + } + + const req = https.request(options, (res) => { + const chunks = [] + res.on('data', (chunk) => chunks.push(chunk)) + res.on('end', () => { + const latency = Date.now() - startTime + const audioBuffer = Buffer.concat(chunks) + + resolve({ + statusCode: res.statusCode, + latency, + size: audioBuffer.length, + cacheHit: res.headers['x-cache-hit'], + xLatency: res.headers['x-latency-ms'], + ttsLatency: res.headers['x-tts-latency-ms'], + slug: res.headers['x-slug'], + charId: res.headers['x-char-id'], + contentType: res.headers['content-type'], + }) + }) + }) + + req.on('error', reject) + req.setTimeout(30000, () => reject(new Error('Timeout'))) + req.write(requestBody) + req.end() + }) +} + +async function runBenchmark(testCase, iterations = 3) { + console.log(`\n=== Testing: ${testCase.name} (${testCase.text.length} chars) ===`) + console.log(`Text: ${testCase.text.substring(0, 50)}...`) + + const results = [] + + for (let i = 0; i < iterations; i++) { + console.log(` Run ${i + 1}:`) + try { + const result = await testTTS(WORKERS_HOST, WORKERS_PATH, testCase.text) + results.push(result) + + const cacheStatus = result.cacheHit === 'true' ? 'HIT' : 'MISS' + console.log(` Status: ${result.statusCode}, Cache: ${cacheStatus}`) + console.log(` Latency: ${result.latency}ms (X-Latency: ${result.xLatency}ms)`) + console.log(` Size: ${(result.size / 1024).toFixed(1)} KB`) + console.log(` Content-Type: ${result.contentType}`) + + if (result.ttsLatency) { + console.log(` TTS Latency: ${result.ttsLatency}ms`) + } + + // Small delay between requests + await new Promise(r => setTimeout(r, 500)) + } catch (error) { + console.log(` Error: ${error.message}`) + } + } + + return results +} + +async function testAllPages() { + console.log('\n=== Testing All 98 Pages (Cache HIT expected) ===') + + const scripts = JSON.parse(fs.readFileSync(SCRIPTS_PATH, 'utf8')) + console.log(`Total pages: ${scripts.length}`) + + const startTime = Date.now() + let successCount = 0 + let errorCount = 0 + let totalLatency = 0 + let cacheHits = 0 + + for (const script of scripts) { + const text = script.line.replace(/\[(neutral|happy|sad|angry|surprised|relaxed)\]/g, '').trim() + + try { + const result = await testTTS(WORKERS_HOST, WORKERS_PATH, text) + + if (result.statusCode === 200) { + successCount++ + totalLatency += result.latency + if (result.cacheHit === 'true') cacheHits++ + + process.stdout.write(` Page ${script.page}: ${result.latency}ms ${result.cacheHit === 'true' ? '(HIT)' : '(MISS)'}\r`) + } else { + errorCount++ + console.log(` Page ${script.page}: Error ${result.statusCode}`) + } + + // Minimal delay + await new Promise(r => setTimeout(r, 50)) + } catch (error) { + errorCount++ + console.log(` Page ${script.page}: ${error.message}`) + } + } + + const totalTime = Date.now() - startTime + + console.log('\n') + console.log('=== Summary ===') + console.log(`Total pages: ${scripts.length}`) + console.log(`Success: ${successCount}, Errors: ${errorCount}`) + console.log(`Cache hits: ${cacheHits}/${successCount} (${(cacheHits/successCount*100).toFixed(1)}%)`) + console.log(`Total time: ${(totalTime/1000).toFixed(1)}s`) + console.log(`Average latency: ${(totalLatency/successCount).toFixed(0)}ms`) + console.log(`Throughput: ${(successCount/(totalTime/1000)).toFixed(1)} req/s`) +} + +async function main() { + const args = process.argv.slice(2) + + console.log('Cloudflare Workers TTS API Compatibility Test') + console.log(`Target: https://${WORKERS_HOST}${WORKERS_PATH}`) + console.log('---') + + // Health check first + try { + const healthRes = await new Promise((resolve, reject) => { + https.get(`https://${WORKERS_HOST}/health`, (res) => { + let data = '' + res.on('data', chunk => data += chunk) + res.on('end', () => resolve({ status: res.statusCode, data })) + }).on('error', reject) + }) + console.log(`Health check: ${healthRes.status} - ${healthRes.data}`) + } catch (e) { + console.log(`Health check failed: ${e.message}`) + process.exit(1) + } + + if (args.includes('--all')) { + // Test all 98 pages + await testAllPages() + } else { + // Quick benchmark with test cases + for (const testCase of TEST_CASES) { + await runBenchmark(testCase, 2) + } + } +} + +main().catch(console.error) diff --git a/public/slides/DHGSVR25-3/theme.css b/public/slides/DHGSVR25-3/theme.css new file mode 100644 index 000000000..0377d55ae --- /dev/null +++ b/public/slides/DHGSVR25-3/theme.css @@ -0,0 +1,292 @@ +/* @theme custom */ +@import 'default'; + +/* Google Fonts - Dela Gothic One */ +@import url('https://fonts.googleapis.com/css2?family=Dela+Gothic+One&display=swap'); + +:root { + --primary-color: #24292f; + --secondary-color: #0969da; + --background-color: #ffffff; + --text-color: #1f2328; + --accent-color: #2ea44f; +} + +section { + background-color: var(--background-color); + color: var(--text-color); + font-family: 'Segoe UI', 'Helvetica Neue', Arial, sans-serif; + font-size: 32px; + line-height: 1.3; + padding: 20px 40px 40px 40px; +} + +h1 { + font-family: 'Dela Gothic One', sans-serif; + color: var(--primary-color); + border-bottom: 3px solid var(--secondary-color); + padding-bottom: 10px; + font-size: 44px; + margin-top: 0; + margin-bottom: 20px; +} + +h2 { + font-size: 36px; + margin-top: 30px; + margin-bottom: 20px; + color: var(--primary-color); +} + +ul, +ol { + margin-left: 1em; + margin-bottom: 20px; + padding-left: 1em; +} + +li { + margin-bottom: 5px; + padding-left: 0.5em; +} + +ol { + list-style-type: decimal; + counter-reset: list; +} + +ol > li { + list-style-type: none; + position: relative; +} + +ol > li::before { + counter-increment: list; + content: counter(list) '.'; + position: absolute; + left: -1.5em; + width: 1.5em; + text-align: right; + color: var(--secondary-color); + font-weight: bold; +} + +ul { + list-style-type: disc; +} + +ul > li { + list-style-type: none; + position: relative; +} + +ul > li::before { + content: '•'; + position: absolute; + left: -1em; + width: 1em; + text-align: center; + color: var(--accent-color); +} + +ol ol, +ul ul, +ol ul, +ul ol { + margin-top: 10px; + margin-bottom: 0; + margin-left: 1em; +} + +img { + border-radius: 8px; + max-width: 95%; + height: auto; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); +} + +code { + background-color: #f6f8fa; + padding: 2px 6px; + border-radius: 4px; + font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; + font-size: 0.9em; + color: #0550ae; +} + +pre { + background-color: #f6f8fa; + padding: 16px; + border-radius: 8px; + overflow-x: auto; +} + +pre code { + background: none; + padding: 0; +} + +a { + color: var(--secondary-color); + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +.columns { + display: flex; + gap: 1.5rem; +} + +.columns > div { + flex: 1; +} + +.columns img { + max-width: 100%; + height: auto; +} + +section.title { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + height: 100%; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; +} + +section.title h1 { + font-family: 'Dela Gothic One', sans-serif; + font-size: 72px; + text-align: center; + border: none; + color: white; + text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); +} + +section.title p { + font-size: 40px; + margin-top: 20px; +} + +section.title strong { + font-size: 44px; +} + +section.end { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + height: 100%; + background: linear-gradient(135deg, #2ea44f 0%, #238636 100%); + color: white; +} + +section.end h1 { + font-family: 'Dela Gothic One', sans-serif; + font-size: 52px; + text-align: center; + border: none; + color: white; +} + +/* ページ番号のスタイル */ +section::after { + content: attr(data-marpit-pagination) '/' attr(data-marpit-pagination-total); + font-size: 16px; + color: var(--text-color); + opacity: 0.5; + right: 30px; + bottom: 20px; +} + +/* 背景画像ありのスライド(右1/2に画像、左1/2にテキスト) */ +section[data-marpit-advanced-background='background'] { + padding: 15px 30px; +} + +/* 背景画像を右上に配置 */ +section[data-marpit-advanced-background='background'] figure img { + object-position: top right !important; +} + +section[data-marpit-advanced-background='background'] h1 { + font-size: 42px; + margin-bottom: 12px; + padding-bottom: 8px; +} + +section[data-marpit-advanced-background='background'] ul, +section[data-marpit-advanced-background='background'] ol { + font-size: 30px; + line-height: 1.4; + margin-bottom: 12px; +} + +section[data-marpit-advanced-background='background'] li { + margin-bottom: 6px; +} + +/* 背景画像なしのスライド - 全幅使用 */ +section:not([data-marpit-advanced-background]) { + /* キャラクター表示スペースを考慮 */ +} + +section:not([data-marpit-advanced-background]) h1 { + font-size: 48px; +} + +section:not([data-marpit-advanced-background]) ul, +section:not([data-marpit-advanced-background]) ol { + font-size: 30px; +} + +/* GitHub風のアクセントカラー */ +.github-green { + color: var(--accent-color); +} + +.github-blue { + color: var(--secondary-color); +} + +/* 強調ボックス */ +blockquote { + background-color: #ddf4ff; + border-left: 4px solid var(--secondary-color); + padding: 16px 20px; + margin: 20px 0; + border-radius: 0 8px 8px 0; +} + +blockquote p { + margin: 0; +} + +/* テーブルスタイル */ +table { + border-collapse: collapse; + width: 100%; + margin: 20px 0; +} + +th, +td { + border: 1px solid #d0d7de; + padding: 12px 16px; + text-align: left; +} + +th { + background-color: #f6f8fa; + font-weight: 600; +} + +tr:nth-child(even) { + background-color: #f6f8fa; +} diff --git a/public/vrm/LuC4.vrm b/public/vrm/LuC4.vrm new file mode 100644 index 000000000..bc01ffb6c Binary files /dev/null and b/public/vrm/LuC4.vrm differ diff --git a/src/components/assistantText.tsx b/src/components/assistantText.tsx index ff1551c3a..2ef564478 100644 --- a/src/components/assistantText.tsx +++ b/src/components/assistantText.tsx @@ -22,7 +22,7 @@ export const AssistantText = ({ message }: { message: string }) => { )}
-
+
{message.replace(/\[([a-zA-Z]*?)\]/g, '')}
diff --git a/src/components/characterPresetMenu.tsx b/src/components/characterPresetMenu.tsx index f4d4b1700..9a9380e28 100644 --- a/src/components/characterPresetMenu.tsx +++ b/src/components/characterPresetMenu.tsx @@ -17,11 +17,12 @@ const CharacterPresetMenu = () => { const store = settingsStore() const selectedPresetIndex = store.selectedPresetIndex const showQuickMenu = store.showQuickMenu + const showControlPanel = store.showControlPanel const { placedImages, reorderAllLayers, getAllLayerItems } = useImagesStore() - // コンポーネントが非表示設定の場合は何も表示しない - if (!showQuickMenu) { + // コンポーネントが非表示設定の場合、またはコントロールパネル非表示時は何も表示しない + if (!showQuickMenu || !showControlPanel) { return null } diff --git a/src/components/githubLink.tsx b/src/components/githubLink.tsx index 12a3e0197..f38ca1096 100644 --- a/src/components/githubLink.tsx +++ b/src/components/githubLink.tsx @@ -6,7 +6,7 @@ export const GitHubLink = () => {
diff --git a/src/components/introduction.tsx b/src/components/introduction.tsx index 3daf059a4..7f74f2d61 100644 --- a/src/components/introduction.tsx +++ b/src/components/introduction.tsx @@ -12,15 +12,23 @@ export const Introduction = () => { const showIntroduction = homeStore((s) => s.showIntroduction) const selectLanguage = settingsStore((s) => s.selectLanguage) + // 環境変数で明示的に非表示が設定されている場合 + const envDisabled = process.env.NEXT_PUBLIC_SHOW_INTRODUCTION === 'false' + const [displayIntroduction, setDisplayIntroduction] = useState(false) const [opened, setOpened] = useState(true) - const [dontShowAgain, setDontShowAgain] = useState(false) + const [dontShowAgain, setDontShowAgain] = useState(envDisabled) const { t } = useTranslation() useEffect(() => { + // 環境変数で非表示の場合は表示しない + if (envDisabled) { + setDisplayIntroduction(false) + return + } setDisplayIntroduction(homeStore.getState().showIntroduction) - }, [showIntroduction]) + }, [showIntroduction, envDisabled]) const updateLanguage = () => { console.log('i18n.language', i18n.language) diff --git a/src/components/menu.tsx b/src/components/menu.tsx index b4559bf47..2a3a4d6e2 100644 --- a/src/components/menu.tsx +++ b/src/components/menu.tsx @@ -99,6 +99,15 @@ export const Menu = () => { setTouchStartTime(null) } + // セッション開始時にスライドモードが有効なら自動でプレゼン開始 + useEffect(() => { + if (slideMode && selectedSlideDocs && !slideVisible) { + console.log('🚀 Auto-starting slide mode') + menuStore.setState({ slideVisible: true }) + slideStore.setState({ autoPlay: true, currentSlide: 0 }) + } + }, [slideMode, selectedSlideDocs, slideVisible]) + useEffect(() => { if (!selectedSlideDocs) return @@ -141,6 +150,13 @@ export const Menu = () => { if ((event.metaKey || event.ctrlKey) && event.key === '.') { setShowSettings((prevState) => !prevState) } + // Ctrl+H: コントロールパネルの表示/非表示切り替え + if ((event.metaKey || event.ctrlKey) && event.key === 'h') { + event.preventDefault() + settingsStore.setState((state) => ({ + showControlPanel: !state.showControlPanel, + })) + } } window.addEventListener('keydown', handleKeyDown) @@ -309,9 +325,14 @@ export const Menu = () => { - menuStore.setState({ slideVisible: !slideVisible }) - } + onClick={() => { + const newVisible = !slideVisible + menuStore.setState({ slideVisible: newVisible }) + // スライドモード開始時にautoPlayをリセット + if (newVisible) { + slideStore.setState({ autoPlay: true, currentSlide: 0 }) + } + }} disabled={slidePlaying} />
@@ -327,8 +348,10 @@ export const Menu = () => { {showSettings && setShowSettings(false)} />} {chatLogMode === CHAT_LOG_MODE.ASSISTANT && latestAssistantMessage && - (!slideMode || !slideVisible) && - showAssistantText && } + showAssistantText && + !(slideMode && slideVisible && slidePlaying) && ( + + )} {showWebcam && navigator.mediaDevices && } {showCapture && } {showPermissionModal && ( diff --git a/src/components/meta.tsx b/src/components/meta.tsx index e37869aa3..205ed735b 100644 --- a/src/components/meta.tsx +++ b/src/components/meta.tsx @@ -4,7 +4,7 @@ export const Meta = () => { const title = 'AITuberKit' const description = 'Webブラウザだけで誰でも簡単にAIキャラと会話したり、Youtubeで配信したりできます。' - const imageUrl = '/ogp.png' + const imageUrl = '/backgrounds/AITuber.png' return ( {title} diff --git a/src/components/slideControls.tsx b/src/components/slideControls.tsx index 59468f848..e80b6e476 100644 --- a/src/components/slideControls.tsx +++ b/src/components/slideControls.tsx @@ -5,9 +5,12 @@ interface SlideControlsProps { currentSlide: number slideCount: number isPlaying: boolean + isReverse?: boolean prevSlide: () => void nextSlide: () => void toggleIsPlaying: () => void + toggleReverse?: () => void + goToLastSlide?: () => void showPlayButton?: boolean // 中央ボタン表示制御用プロパティ (オプショナル) } @@ -15,9 +18,12 @@ const SlideControls: React.FC = ({ currentSlide, slideCount, isPlaying, + isReverse = false, prevSlide, nextSlide, toggleIsPlaying, + toggleReverse, + goToLastSlide, showPlayButton = true, // デフォルトは表示する }) => { return ( @@ -49,6 +55,30 @@ const SlideControls: React.FC = ({ isProcessing={false} className="bg-primary hover:bg-primary-hover disabled:bg-primary-disabled text-theme rounded-2xl py-2 px-4 text-center" // mx-16削除 /> + {/* 最終ページへジャンプ */} + {goToLastSlide && ( + + )} + {/* 逆再生モード切り替え */} + {toggleReverse && ( + + )}
) } diff --git a/src/components/slideText.tsx b/src/components/slideText.tsx index 7e0feb344..4544e69e5 100644 --- a/src/components/slideText.tsx +++ b/src/components/slideText.tsx @@ -5,7 +5,7 @@ export const SlideText = () => { return (
-
+
{slideMessages[0] || ' '}
diff --git a/src/components/slides.tsx b/src/components/slides.tsx index 59dbbe7e3..871af601e 100644 --- a/src/components/slides.tsx +++ b/src/components/slides.tsx @@ -1,11 +1,258 @@ -import React, { useEffect, useState, useCallback } from 'react' +import React, { useEffect, useState, useCallback, useRef } from 'react' import slideStore from '@/features/stores/slide' import homeStore from '@/features/stores/home' +import settingsStore from '@/features/stores/settings' import { speakMessageHandler } from '@/features/chat/handlers' import { SpeakQueue } from '@/features/messages/speakQueue' +import { Live2DHandler } from '@/features/messages/live2dHandler' +import { EmotionType } from '@/features/messages/messages' import SlideContent from './slideContent' import SlideControls from './slideControls' +// gtag型定義 +declare global { + interface Window { + gtag?: ( + command: string, + action: string, + params?: Record + ) => void + } +} + +// Google Analytics イベント送信 +const trackSlideView = ( + slideDocs: string, + page: number, + totalPages: number +) => { + if (typeof window !== 'undefined' && window.gtag) { + window.gtag('event', 'slide_view', { + slide_docs: slideDocs, + page_number: page, + total_pages: totalPages, + progress_percent: Math.round((page / totalPages) * 100), + }) + } +} + +// 最終ページ到達時のSlack通知 +const notifySlideCompletion = async ( + slideDocs: string, + totalPages: number, + startTime: Date | null +): Promise => { + try { + const endTime = new Date() + const startTimeStr = startTime + ? startTime.toLocaleString('ja-JP', { timeZone: 'Asia/Tokyo' }) + : '不明' + const endTimeStr = endTime.toLocaleString('ja-JP', { + timeZone: 'Asia/Tokyo', + }) + + // 経過時間を計算 + let durationStr = '不明' + if (startTime) { + const durationMs = endTime.getTime() - startTime.getTime() + const minutes = Math.floor(durationMs / 60000) + const seconds = Math.floor((durationMs % 60000) / 1000) + durationStr = `${minutes}分${seconds}秒` + } + + await fetch('/api/slack-notify', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + slideDocs, + totalPages, + userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : '', + startTime: startTimeStr, + endTime: endTimeStr, + duration: durationStr, + }), + }) + console.log('%c📨 Slack notification sent', 'color: #e01e5a') + } catch (error) { + console.error('Failed to send Slack notification:', error) + } +} + +// 感情タグを解析して最初の感情を取得 +const parseFirstEmotion = (line: string): EmotionType => { + const match = line.match(/\[(neutral|happy|sad|angry|surprised|relaxed)\]/) + return (match ? match[1] : 'neutral') as EmotionType +} + +// AICU API の音声ホスティングURL +const AICU_AUDIO_BASE_URL = 'https://api.aicu.ai/v1/audio' + +// 事前生成音声ファイルのパスを取得 +const getPreGeneratedAudioPath = (slideDocs: string, page: number): string => { + // AICU API から音声を取得 + return `${AICU_AUDIO_BASE_URL}/${slideDocs}/page${page}.mp3` +} + +// 事前生成音声ファイルが存在するかチェック +const checkAudioExists = async (path: string): Promise => { + try { + const response = await fetch(path, { method: 'HEAD' }) + return response.ok + } catch { + return false + } +} + +// 音声ファイルのプリロードキャッシュ +const audioCache = new Map() + +// 音声ファイルをプリロード(1ページ先まで) +const preloadAudio = async ( + slideDocs: string, + currentPage: number, + totalPages: number +): Promise => { + const pagesToPreload = [currentPage, currentPage + 1].filter( + (p) => p >= 0 && p < totalPages + ) + + slideStore.setState({ + audioPreload: { + isLoading: true, + progress: 0, + loadedPages: new Set(), + error: null, + }, + }) + + const loadedPages = new Set() + + for (let i = 0; i < pagesToPreload.length; i++) { + const page = pagesToPreload[i] + const audioPath = getPreGeneratedAudioPath(slideDocs, page) + + try { + // キャッシュにあればスキップ + if (audioCache.has(audioPath)) { + loadedPages.add(page) + continue + } + + const exists = await checkAudioExists(audioPath) + if (exists) { + const response = await fetch(audioPath) + if (response.ok) { + const buffer = await response.arrayBuffer() + audioCache.set(audioPath, buffer) + loadedPages.add(page) + } + } + } catch (error) { + console.warn(`Failed to preload audio for page ${page}:`, error) + // エラーでも続行(先読み失敗は致命的ではない) + } + + // プログレス更新 + slideStore.setState({ + audioPreload: { + isLoading: true, + progress: Math.round(((i + 1) / pagesToPreload.length) * 100), + loadedPages, + error: null, + }, + }) + } + + slideStore.setState({ + audioPreload: { + isLoading: false, + progress: 100, + loadedPages, + error: null, + }, + }) +} + +// キャッシュから音声を取得 +const getCachedAudio = (audioPath: string): ArrayBuffer | undefined => { + return audioCache.get(audioPath) +} + +// 音声の長さを取得(秒) +const getAudioDuration = async (audioBuffer: ArrayBuffer): Promise => { + const audioContext = new AudioContext() + const decodedBuffer = await audioContext.decodeAudioData(audioBuffer.slice(0)) + const duration = decodedBuffer.duration + await audioContext.close() + return duration +} + +// テキストを句読点で分割 +const splitTextByPunctuation = (text: string): string[] => { + // 句読点で分割(。!?、で区切るが、区切り文字は含める) + const segments = text.split(/(?<=[。!?、])/g).filter((s) => s.trim()) + // 短すぎるセグメントは次と結合 + const result: string[] = [] + let current = '' + for (const segment of segments) { + current += segment + // 10文字以上、または最後の句点(。!?)で区切る + if (current.length >= 10 || /[。!?]$/.test(current)) { + result.push(current.trim()) + current = '' + } + } + if (current.trim()) { + result.push(current.trim()) + } + return result.length > 0 ? result : [text] +} + +// 事前生成音声を再生(キャッシュ優先)- 音声長を返す +const playPreGeneratedAudio = async ( + audioPath: string, + emotion: EmotionType +): Promise => { + const ss = settingsStore.getState() + const hs = homeStore.getState() + + try { + // キャッシュから取得を試みる + let audioBuffer = getCachedAudio(audioPath) + + if (!audioBuffer) { + // キャッシュになければfetch + const response = await fetch(audioPath) + if (!response.ok) throw new Error('Audio file not found') + audioBuffer = await response.arrayBuffer() + // キャッシュに保存 + audioCache.set(audioPath, audioBuffer) + } + + // 音声の長さを取得 + const duration = await getAudioDuration(audioBuffer) + + homeStore.setState({ isSpeaking: true }) + + // VRM/Live2D に音声を再生させる + if (ss.modelType === 'live2d') { + await Live2DHandler.speak( + audioBuffer, + { message: '', emotion }, + true // MP3はデコードが必要 + ) + } else if (hs.viewer.model) { + await hs.viewer.model.speak(audioBuffer, { message: '', emotion }, true) + } + + homeStore.setState({ isSpeaking: false }) + return duration + } catch (error) { + console.error('Failed to play pre-generated audio:', error) + throw error + } +} + interface SlidesProps { markdown: string } @@ -16,13 +263,23 @@ export const goToSlide = (index: number) => { }) } -const Slides: React.FC = ({ markdown }) => { +const Slides: React.FC = () => { const [marpitContainer, setMarpitContainer] = useState(null) const isPlaying = slideStore((state) => state.isPlaying) + const isReverse = slideStore((state) => state.isReverse) const currentSlide = slideStore((state) => state.currentSlide) const selectedSlideDocs = slideStore((state) => state.selectedSlideDocs) + const autoPlay = slideStore((state) => state.autoPlay) + const audioPreload = slideStore((state) => state.audioPreload) const chatProcessingCount = homeStore((s) => s.chatProcessingCount) + const showControlPanel = settingsStore((s) => s.showControlPanel) const [slideCount, setSlideCount] = useState(0) + const [autoPlayTriggered, setAutoPlayTriggered] = useState(false) + const [waitingForUserGesture, setWaitingForUserGesture] = useState(false) + const [completionNotified, setCompletionNotified] = useState(false) + const [presentationStartTime, setPresentationStartTime] = + useState(null) + const prevChatProcessingCountRef = useRef(chatProcessingCount) useEffect(() => { const currentMarpitContainer = document.querySelector('.marpit') @@ -40,7 +297,14 @@ const Slides: React.FC = ({ markdown }) => { }, [currentSlide, marpitContainer]) useEffect(() => { + // selectedSlideDocsが空の場合はスキップ + if (!selectedSlideDocs) { + console.log('⏳ Waiting for slide selection...') + return + } + const convertMarkdown = async () => { + console.log(`📑 Loading slides: ${selectedSlideDocs}`) const response = await fetch('/api/convertMarkdown', { method: 'POST', headers: { @@ -48,6 +312,12 @@ const Slides: React.FC = ({ markdown }) => { }, body: JSON.stringify({ slideName: selectedSlideDocs }), }) + + if (!response.ok) { + console.error(`❌ Failed to load slides: ${response.status}`) + return + } + const data = await response.json() // HTMLをパースしてmarpit要素を取得 @@ -60,6 +330,7 @@ const Slides: React.FC = ({ markdown }) => { if (marpitElement) { const slides = marpitElement.querySelectorAll(':scope > svg') setSlideCount(slides.length) + console.log(`✅ Slides loaded: ${slides.length} pages`) // 初期状態で最初のスライドを表示 slides.forEach((slide, i) => { @@ -90,6 +361,13 @@ const Slides: React.FC = ({ markdown }) => { div.marpit > svg > foreignObject > section { padding: 2em; } + /* 背景画像を右上に配置 */ + div.marpit > svg > foreignObject > section figure[data-marpit-advanced-background-container] { + align-items: flex-start !important; + } + div.marpit > svg > foreignObject > section figure img { + object-position: top !important; + } ` const styleElement = document.createElement('style') styleElement.textContent = customStyle @@ -102,7 +380,7 @@ const Slides: React.FC = ({ markdown }) => { }, []) const readSlide = useCallback( - (slideIndex: number) => { + async (slideIndex: number) => { const getCurrentLines = () => { const scripts = require( `../../public/slides/${selectedSlideDocs}/scripts.json` @@ -114,33 +392,168 @@ const Slides: React.FC = ({ markdown }) => { } const currentLines = getCurrentLines() - console.log(currentLines) - speakMessageHandler(currentLines) + + // 事前生成音声ファイルのパスをチェック + const audioPath = getPreGeneratedAudioPath(selectedSlideDocs, slideIndex) + const audioExists = await checkAudioExists(audioPath) + + if (audioExists) { + // 事前生成音声があれば再生 + console.log( + `%c🎵 [MP3] Slide ${slideIndex}: ${audioPath}`, + 'color: #4ade80; font-weight: bold' + ) + const emotion = parseFirstEmotion(currentLines) + + // 感情タグを除去して字幕を設定 + const subtitleText = currentLines.replace( + /\[(neutral|happy|sad|angry|surprised|relaxed)\]/g, + '' + ) + + // テキストを句読点で分割 + const subtitleSegments = splitTextByPunctuation(subtitleText) + console.log( + `%c📝 [MP3] Segments: ${subtitleSegments.length}`, + 'color: #4ade80' + ) + + // chatProcessingCount を増やして再生開始 + homeStore.getState().incrementChatProcessingCount() + + // 字幕タイマーのIDを保持 + const subtitleTimers: NodeJS.Timeout[] = [] + let subtitleCleanedUp = false + + // 字幕クリーンアップ関数 + const cleanupSubtitles = () => { + if (subtitleCleanedUp) return + subtitleCleanedUp = true + subtitleTimers.forEach((timer) => clearTimeout(timer)) + homeStore.setState({ slideMessages: [] }) + } + + try { + // 最初の字幕を表示 + homeStore.setState({ slideMessages: [subtitleSegments[0]] }) + + // 音声再生開始(durationを取得) + const audioPromise = playPreGeneratedAudio(audioPath, emotion) + + // 音声の長さを先に取得してタイマーをセット + const audioBuffer = getCachedAudio(audioPath) + if (audioBuffer && subtitleSegments.length > 1) { + const duration = await getAudioDuration(audioBuffer) + const segmentDuration = (duration * 1000) / subtitleSegments.length + + // 各セグメントの表示タイミングをスケジュール + for (let i = 1; i < subtitleSegments.length; i++) { + const timer = setTimeout(() => { + if (!subtitleCleanedUp) { + homeStore.setState({ slideMessages: [subtitleSegments[i]] }) + } + }, segmentDuration * i) + subtitleTimers.push(timer) + } + } + + // 音声再生完了を待つ + await audioPromise + } catch (error) { + // 音声再生に失敗した場合は TTS にフォールバック + console.log( + `%c⚠️ [MP3→API] Fallback to TTS API: ${error}`, + 'color: #fbbf24; font-weight: bold' + ) + cleanupSubtitles() + // プリ生成音声の処理が終わったのでカウントを減らす + homeStore.getState().decrementChatProcessingCount() + // TTS は自身で chatProcessingCount を管理する + speakMessageHandler(currentLines) + return + } + + // 再生完了後に字幕をクリア + cleanupSubtitles() + // 再生完了後にカウントを減らす + console.log(`%c✅ [MP3] Slide ${slideIndex} finished`, 'color: #4ade80') + homeStore.getState().decrementChatProcessingCount() + } else { + // なければ TTS API を使用 + console.log( + `%c🔊 [API] Slide ${slideIndex}: Using TTS API`, + 'color: #60a5fa; font-weight: bold' + ) + console.log( + `%c📝 [API] Text: ${currentLines.substring(0, 50)}...`, + 'color: #60a5fa' + ) + speakMessageHandler(currentLines) + } }, [selectedSlideDocs] ) const nextSlide = useCallback(() => { - slideStore.setState((state) => { - const newSlide = Math.min(state.currentSlide + 1, slideCount - 1) - if (isPlaying) { - readSlide(newSlide) - } - return { currentSlide: newSlide } - }) - }, [isPlaying, readSlide, slideCount]) + const state = slideStore.getState() + const newSlide = Math.min(state.currentSlide + 1, slideCount - 1) + slideStore.setState({ currentSlide: newSlide }) + return newSlide + }, [slideCount]) + + // スライド変更時にgtagでトラッキング + useEffect(() => { + if (slideCount > 0 && selectedSlideDocs) { + trackSlideView(selectedSlideDocs, currentSlide, slideCount) + } + }, [currentSlide, slideCount, selectedSlideDocs]) useEffect(() => { - // 最後のスライドに達した場合、isPlayingをfalseに設定 - if (currentSlide === slideCount - 1 && chatProcessingCount === 0) { - slideStore.setState({ isPlaying: false }) + // 最後/最初のスライドに達した場合、isPlayingをfalseに設定 + if (isReverse) { + if (currentSlide === 0 && chatProcessingCount === 0) { + slideStore.setState({ isPlaying: false }) + } + } else { + if (currentSlide === slideCount - 1 && chatProcessingCount === 0) { + slideStore.setState({ isPlaying: false }) + // 最終ページ到達時にSlack通知(1回のみ) + if (!completionNotified && slideCount > 0) { + setCompletionNotified(true) + notifySlideCompletion( + selectedSlideDocs, + slideCount, + presentationStartTime + ) + // gtag で完了イベントも送信 + if (typeof window !== 'undefined' && window.gtag) { + window.gtag('event', 'slide_completed', { + slide_docs: selectedSlideDocs, + total_pages: slideCount, + }) + } + // 終了時にコントロールパネルを表示して自由会話モードへ + settingsStore.setState({ showControlPanel: true }) + slideStore.setState({ freeConversationMode: true }) + console.log('🎤 Free conversation mode enabled') + } + } } - }, [currentSlide, slideCount, chatProcessingCount]) + }, [ + currentSlide, + slideCount, + chatProcessingCount, + isReverse, + completionNotified, + selectedSlideDocs, + presentationStartTime, + ]) const prevSlide = useCallback(() => { - slideStore.setState((state) => ({ - currentSlide: Math.max(state.currentSlide - 1, 0), - })) + const state = slideStore.getState() + const newSlide = Math.max(state.currentSlide - 1, 0) + slideStore.setState({ currentSlide: newSlide }) + return newSlide }, []) const toggleIsPlaying = () => { @@ -156,15 +569,91 @@ const Slides: React.FC = ({ markdown }) => { } } + const toggleReverse = () => { + slideStore.setState((state) => ({ + isReverse: !state.isReverse, + })) + } + + const goToLastSlide = useCallback(() => { + slideStore.setState({ currentSlide: slideCount - 1 }) + }, [slideCount]) + + // chatProcessingCount が 0 に変化したときのみ次/前のスライドに進む useEffect(() => { - if ( - chatProcessingCount === 0 && - isPlaying && - currentSlide < slideCount - 1 - ) { - nextSlide() + const prevCount = prevChatProcessingCountRef.current + prevChatProcessingCountRef.current = chatProcessingCount + + // 0 に変化したときのみ処理(無限ループ防止) + if (prevCount > 0 && chatProcessingCount === 0 && isPlaying) { + if (isReverse) { + if (currentSlide > 0) { + const newSlide = prevSlide() + readSlide(newSlide) + } + } else { + if (currentSlide < slideCount - 1) { + const newSlide = nextSlide() + readSlide(newSlide) + } + } + } + }, [ + chatProcessingCount, + isPlaying, + isReverse, + currentSlide, + slideCount, + nextSlide, + prevSlide, + readSlide, + ]) + + // autoPlayがtrueになったらautoPlayTriggeredをリセット + useEffect(() => { + if (autoPlay) { + setAutoPlayTriggered(false) + } + }, [autoPlay]) + + // 自動再生:スライドロード完了後にユーザージェスチャーを待つ + useEffect(() => { + if (slideCount > 0 && autoPlay && !autoPlayTriggered && !isPlaying) { + console.log('🚀 Auto-play: Waiting for user gesture') + setAutoPlayTriggered(true) + slideStore.setState({ autoPlay: false, currentSlide: 0 }) + setWaitingForUserGesture(true) + } + }, [slideCount, autoPlay, autoPlayTriggered, isPlaying]) + + // ユーザージェスチャーで再生開始 + const handleStartPresentation = useCallback(() => { + console.log('▶️ User gesture received, starting presentation') + setWaitingForUserGesture(false) + setPresentationStartTime(new Date()) + slideStore.setState({ isPlaying: true }) + readSlide(0) + }, [readSlide]) + + // 音声ファイルの先読み(現在のスライド + 次のスライドのみ) + useEffect(() => { + if (slideCount > 0 && selectedSlideDocs) { + // 非同期でプリロード(try-catch でエラーハンドリング済み) + preloadAudio(selectedSlideDocs, currentSlide, slideCount).catch( + (error) => { + console.error('Audio preload failed:', error) + slideStore.setState({ + audioPreload: { + isLoading: false, + progress: 0, + loadedPages: new Set(), + error: String(error), + }, + }) + } + ) } - }, [chatProcessingCount, isPlaying, nextSlide, currentSlide, slideCount]) + }, [currentSlide, slideCount, selectedSlideDocs]) // スライドの縦のサイズを70%に制限し、アスペクト比を維持 const calculateSlideSize = () => { @@ -185,7 +674,7 @@ const Slides: React.FC = ({ markdown }) => { return (
= ({ markdown }) => { style={{ width: slideSize.width, height: slideSize.height, - margin: '0 auto', + marginLeft: '2%', position: 'relative', }} > @@ -206,20 +695,84 @@ const Slides: React.FC = ({ markdown }) => {
+ {/* 音声プリロード進捗表示 */} + {audioPreload.isLoading && ( +
+
+
+ )}
+ + {/* クリックして開始モーダル */} + {waitingForUserGesture && ( +
+
+ ▶ クリックして開始 +
+
+ )}
) } diff --git a/src/components/vrmViewer.tsx b/src/components/vrmViewer.tsx index 876c0940c..76f08cb76 100644 --- a/src/components/vrmViewer.tsx +++ b/src/components/vrmViewer.tsx @@ -1,9 +1,37 @@ -import { useCallback } from 'react' +import { useCallback, useEffect } from 'react' import homeStore from '@/features/stores/home' import settingsStore from '@/features/stores/settings' export default function VrmViewer() { + // Ctrl+S でカメラ位置を保存してログ出力 + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if ((e.ctrlKey || e.metaKey) && e.key === 's') { + e.preventDefault() + const { viewer } = homeStore.getState() + viewer.saveCameraPosition() + const { characterPosition, characterRotation } = + settingsStore.getState() + + // 環境変数用の文字列を生成 + const envConfig = `NEXT_PUBLIC_CHARACTER_POSITION_X=${characterPosition.x.toFixed(3)} +NEXT_PUBLIC_CHARACTER_POSITION_Y=${characterPosition.y.toFixed(3)} +NEXT_PUBLIC_CHARACTER_POSITION_Z=${characterPosition.z.toFixed(3)} +NEXT_PUBLIC_CHARACTER_SCALE=${characterPosition.scale.toFixed(3)} +NEXT_PUBLIC_CHARACTER_ROTATION_X=${characterRotation.x.toFixed(3)} +NEXT_PUBLIC_CHARACTER_ROTATION_Y=${characterRotation.y.toFixed(3)} +NEXT_PUBLIC_CHARACTER_ROTATION_Z=${characterRotation.z.toFixed(3)}` + + // コンソールに出力 + console.log('📍 Character Position Saved:') + console.log(envConfig) + } + } + window.addEventListener('keydown', handleKeyDown) + return () => window.removeEventListener('keydown', handleKeyDown) + }, []) + const canvasRef = useCallback((canvas: HTMLCanvasElement) => { if (canvas) { const { viewer } = homeStore.getState() diff --git a/src/features/chat/handlers.ts b/src/features/chat/handlers.ts index 6b03b9d04..aae7d4cfa 100644 --- a/src/features/chat/handlers.ts +++ b/src/features/chat/handlers.ts @@ -18,6 +18,36 @@ import { } from '@/features/memory/memoryStoreSync' import { THINKING_MARKER } from '@/features/chat/vercelAIChat' +// 自由会話モードの会話をSlackに報告 +const reportConversationToSlack = async ( + userMessage: string, + assistantMessage: string +): Promise => { + const sls = slideStore.getState() + + // 自由会話モードでなければスキップ + if (!sls.freeConversationMode) return + + try { + await fetch('/api/slack-conversation', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + slideDocs: sls.selectedSlideDocs, + userMessage, + assistantMessage, + userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : '', + timestamp: new Date().toLocaleString('ja-JP', { + timeZone: 'Asia/Tokyo', + }), + }), + }) + console.log('%c📨 Slack conversation reported', 'color: #e01e5a') + } catch (error) { + console.error('Failed to report conversation to Slack:', error) + } +} + // セッションIDを生成する関数 const generateSessionId = () => generateMessageId() diff --git a/src/features/constants/settings.ts b/src/features/constants/settings.ts index 35b8ce6fd..9506c505f 100644 --- a/src/features/constants/settings.ts +++ b/src/features/constants/settings.ts @@ -96,6 +96,7 @@ export type AIVoice = | 'cartesia' | 'openai' | 'azure' + | 'aicu' export type Language = (typeof LANGUAGES)[number] diff --git a/src/features/messages/speakCharacter.ts b/src/features/messages/speakCharacter.ts index 10243f357..1743100f0 100644 --- a/src/features/messages/speakCharacter.ts +++ b/src/features/messages/speakCharacter.ts @@ -17,6 +17,7 @@ import { synthesizeVoiceAzureOpenAIApi } from './synthesizeVoiceAzureOpenAI' import toastStore from '@/features/stores/toast' import i18next from 'i18next' import { SpeakQueue } from './speakQueue' +import { synthesizeVoiceAicuApi } from './synthesizeVoiceAicu' import { Live2DHandler } from './live2dHandler' import { PNGTuberHandler } from '@/features/pngTuber/pngTuberHandler' import { @@ -176,6 +177,8 @@ async function synthesizeVoice( ss.openaiTTSVoice, ss.openaiTTSSpeed ) + case 'aicu': + return await synthesizeVoiceAicuApi(talk, ss.aicuSlug) default: return null } @@ -378,6 +381,7 @@ export const testVoice = async (voiceType: AIVoice, customText?: string) => { cartesia: 'Cartesiaを使用します', openai: 'OpenAI TTSを使用します', azure: 'Azure TTSを使用します', + aicu: 'AICU TTSを使用します', } const message = customText || defaultMessages[voiceType] diff --git a/src/features/messages/synthesizeVoiceAicu.ts b/src/features/messages/synthesizeVoiceAicu.ts new file mode 100644 index 000000000..0a5e67eef --- /dev/null +++ b/src/features/messages/synthesizeVoiceAicu.ts @@ -0,0 +1,47 @@ +import { Talk } from './messages' + +export async function synthesizeVoiceAicuApi( + talk: Talk, + aicuSlug: string = 'luc4' +) { + try { + const body = { + message: talk.message, + slug: aicuSlug, + } + + const res = await fetch('/api/tts-aicu', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + }) + + if (!res.ok) { + throw new Error( + `AICU Text-to-Speech APIからの応答が異常です。ステータスコード: ${res.status}` + ) + } + + const data = await res.json() + + // Base64文字列をデコードしてArrayBufferに変換 + const binaryStr = atob(data.audio) + const uint8Array = new Uint8Array(binaryStr.length) + for (let i = 0; i < binaryStr.length; i++) { + uint8Array[i] = binaryStr.charCodeAt(i) + } + const arrayBuffer: ArrayBuffer = uint8Array.buffer + + return arrayBuffer + } catch (error) { + if (error instanceof Error) { + throw new Error( + `AICU Text-to-Speechでエラーが発生しました: ${error.message}` + ) + } else { + throw new Error('AICU Text-to-Speechで不明なエラーが発生しました') + } + } +} diff --git a/src/features/stores/home.ts b/src/features/stores/home.ts index 77efb09f5..389524f54 100644 --- a/src/features/stores/home.ts +++ b/src/features/stores/home.ts @@ -178,6 +178,19 @@ const homeStore = create()( chatLog: messageSelectors.cutImageMessage(chatLog), showIntroduction, }), + merge: (persistedState, currentState) => { + const persisted = persistedState as Partial + // 環境変数で明示的に false が設定されている場合は優先 + const envShowIntroduction = + process.env.NEXT_PUBLIC_SHOW_INTRODUCTION !== 'false' + return { + ...currentState, + chatLog: persisted?.chatLog || [], + showIntroduction: envShowIntroduction + ? (persisted?.showIntroduction ?? true) + : false, + } + }, onRehydrateStorage: () => (state) => { if (state) { lastSavedLogLength = state.chatLog.length diff --git a/src/features/stores/settings.ts b/src/features/stores/settings.ts index 130fb053d..69047fd75 100644 --- a/src/features/stores/settings.ts +++ b/src/features/stores/settings.ts @@ -131,6 +131,7 @@ interface ModelProvider extends Live2DSettings { openaiTTSVoice: OpenAITTSVoice openaiTTSModel: OpenAITTSModel openaiTTSSpeed: number + aicuSlug: string } interface Integrations { @@ -464,17 +465,18 @@ const getInitialValuesFromEnv = (): SettingsState => ({ selectedLive2DPath: process.env.NEXT_PUBLIC_SELECTED_LIVE2D_PATH || '/live2d/nike01/nike01.model3.json', - fixedCharacterPosition: false, + fixedCharacterPosition: + process.env.NEXT_PUBLIC_FIXED_CHARACTER_POSITION === 'true', characterPosition: { - x: 0, - y: 0, - z: 0, - scale: 1, + x: parseFloat(process.env.NEXT_PUBLIC_CHARACTER_POSITION_X || '0') || 0, + y: parseFloat(process.env.NEXT_PUBLIC_CHARACTER_POSITION_Y || '0') || 0, + z: parseFloat(process.env.NEXT_PUBLIC_CHARACTER_POSITION_Z || '0') || 0, + scale: parseFloat(process.env.NEXT_PUBLIC_CHARACTER_SCALE || '1') || 1, }, characterRotation: { - x: 0, - y: 0, - z: 0, + x: parseFloat(process.env.NEXT_PUBLIC_CHARACTER_ROTATION_X || '0') || 0, + y: parseFloat(process.env.NEXT_PUBLIC_CHARACTER_ROTATION_Y || '0') || 0, + z: parseFloat(process.env.NEXT_PUBLIC_CHARACTER_ROTATION_Z || '0') || 0, }, lightingIntensity: parseFloat(process.env.NEXT_PUBLIC_LIGHTING_INTENSITY || '1.0') || 1.0, @@ -584,6 +586,9 @@ const getInitialValuesFromEnv = (): SettingsState => ({ // Custom model toggle customModel: process.env.NEXT_PUBLIC_CUSTOM_MODEL === 'true', + // AICU TTS settings + aicuSlug: process.env.NEXT_PUBLIC_AICU_SLUG || 'luc4', + // Settings modelType: (process.env.NEXT_PUBLIC_MODEL_TYPE as 'vrm' | 'live2d' | 'pngtuber') || @@ -789,6 +794,7 @@ const settingsStore = create()( characterPosition: state.characterPosition, characterRotation: state.characterRotation, lightingIntensity: state.lightingIntensity, + aicuSlug: state.aicuSlug, modelType: state.modelType, selectedPNGTuberPath: state.selectedPNGTuberPath, pngTuberSensitivity: state.pngTuberSensitivity, diff --git a/src/features/stores/slide.ts b/src/features/stores/slide.ts index 211240a69..4352620fc 100644 --- a/src/features/stores/slide.ts +++ b/src/features/stores/slide.ts @@ -1,22 +1,53 @@ import { create } from 'zustand' import { persist } from 'zustand/middleware' +interface AudioPreloadState { + isLoading: boolean + progress: number // 0-100 + loadedPages: Set + error: string | null +} + interface SlideState { isPlaying: boolean + isReverse: boolean currentSlide: number selectedSlideDocs: string + autoPlay: boolean + audioPreload: AudioPreloadState + freeConversationMode: boolean // プレゼン終了後の自由会話モード } +const defaultSlideDocs = + process.env.NEXT_PUBLIC_DEFAULT_SLIDE_DOCS || 'DHGSVR25-3' + const slideStore = create()( persist( - (set, get) => ({ + (): SlideState => ({ isPlaying: false, + isReverse: false, currentSlide: 0, - selectedSlideDocs: '', + selectedSlideDocs: defaultSlideDocs, + autoPlay: true, + audioPreload: { + isLoading: false, + progress: 0, + loadedPages: new Set(), + error: null, + }, + freeConversationMode: false, }), { name: 'aitube-kit-slide', partialize: (state) => ({ selectedSlideDocs: state.selectedSlideDocs }), + merge: (persistedState, currentState) => { + const persisted = persistedState as Partial + return { + ...currentState, + // 空の場合はデフォルト値を使用 + selectedSlideDocs: persisted?.selectedSlideDocs || defaultSlideDocs, + } + }, } ) ) diff --git a/src/pages/_document.tsx b/src/pages/_document.tsx index 9c987e329..699e2e893 100644 --- a/src/pages/_document.tsx +++ b/src/pages/_document.tsx @@ -1,4 +1,7 @@ import { Html, Head, Main, NextScript } from 'next/document' +import Script from 'next/script' + +const GA_MEASUREMENT_ID = process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID export default function Document() { return ( @@ -18,6 +21,23 @@ export default function Document() {
+ {/* Google Analytics */} + {GA_MEASUREMENT_ID && ( + <> + + + )} ) diff --git a/src/pages/api/slack-conversation.ts b/src/pages/api/slack-conversation.ts new file mode 100644 index 000000000..d9b1369ad --- /dev/null +++ b/src/pages/api/slack-conversation.ts @@ -0,0 +1,125 @@ +import type { NextApiRequest, NextApiResponse } from 'next' + +// モバイルデバイス判定 +const isMobileDevice = (userAgent: string): boolean => { + return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test( + userAgent + ) +} + +// デバイスタイプを判定 +const getDeviceType = (userAgent: string): string => { + if (/iPhone/i.test(userAgent)) return '📱 iPhone' + if (/iPad/i.test(userAgent)) return '📱 iPad' + if (/Android/i.test(userAgent)) { + if (/Mobile/i.test(userAgent)) return '📱 Android (Mobile)' + return '📱 Android (Tablet)' + } + if (/Macintosh/i.test(userAgent)) return '💻 Mac' + if (/Windows/i.test(userAgent)) return '💻 Windows' + if (/Linux/i.test(userAgent)) return '💻 Linux' + return '🖥️ Unknown' +} + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'POST') { + return res.status(405).json({ error: 'Method not allowed' }) + } + + const webhookUrl = process.env.SLACK_WEBHOOK_URL + + if (!webhookUrl) { + return res.status(500).json({ error: 'Slack webhook URL not configured' }) + } + + try { + const { slideDocs, userMessage, assistantMessage, userAgent, timestamp } = + req.body + + // IPアドレスを取得(Vercel/プロキシ対応) + const forwarded = req.headers['x-forwarded-for'] + const ip = + (typeof forwarded === 'string' + ? forwarded.split(',')[0] + : forwarded?.[0]) || + req.socket?.remoteAddress || + 'Unknown' + + // デバイス情報 + const deviceType = getDeviceType(userAgent || '') + const isMobile = isMobileDevice(userAgent || '') + + const message = { + text: `💬 自由会話モード - Q&A`, + blocks: [ + { + type: 'header', + text: { + type: 'plain_text', + text: '💬 自由会話モード - 質問と回答', + emoji: true, + }, + }, + { + type: 'section', + fields: [ + { + type: 'mrkdwn', + text: `*スライド:*\n${slideDocs}`, + }, + { + type: 'mrkdwn', + text: `*時刻:*\n${timestamp}`, + }, + ], + }, + { + type: 'divider', + }, + { + type: 'section', + text: { + type: 'mrkdwn', + text: `*🙋 ユーザー質問:*\n>${userMessage.replace(/\n/g, '\n>')}`, + }, + }, + { + type: 'section', + text: { + type: 'mrkdwn', + text: `*🤖 AI回答:*\n${assistantMessage.substring(0, 500)}${assistantMessage.length > 500 ? '...' : ''}`, + }, + }, + { + type: 'context', + elements: [ + { + type: 'mrkdwn', + text: `${deviceType} | ${isMobile ? '📱 モバイル' : '💻 デスクトップ'} | IP: \`${ip}\``, + }, + ], + }, + ], + } + + const response = await fetch(webhookUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(message), + }) + + if (!response.ok) { + throw new Error(`Slack API error: ${response.status}`) + } + + res.status(200).json({ success: true }) + } catch (error) { + console.error('Slack conversation notification error:', error) + res.status(500).json({ error: 'Failed to send notification' }) + } +} diff --git a/src/pages/api/slack-notify.ts b/src/pages/api/slack-notify.ts new file mode 100644 index 000000000..595492da1 --- /dev/null +++ b/src/pages/api/slack-notify.ts @@ -0,0 +1,157 @@ +import type { NextApiRequest, NextApiResponse } from 'next' + +// モバイルデバイス判定 +const isMobileDevice = (userAgent: string): boolean => { + return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test( + userAgent + ) +} + +// デバイスタイプを判定 +const getDeviceType = (userAgent: string): string => { + if (/iPhone/i.test(userAgent)) return '📱 iPhone' + if (/iPad/i.test(userAgent)) return '📱 iPad' + if (/Android/i.test(userAgent)) { + if (/Mobile/i.test(userAgent)) return '📱 Android (Mobile)' + return '📱 Android (Tablet)' + } + if (/Macintosh/i.test(userAgent)) return '💻 Mac' + if (/Windows/i.test(userAgent)) return '💻 Windows' + if (/Linux/i.test(userAgent)) return '💻 Linux' + return '🖥️ Unknown' +} + +// ブラウザを判定 +const getBrowser = (userAgent: string): string => { + if (/Chrome/i.test(userAgent) && !/Edg/i.test(userAgent)) return 'Chrome' + if (/Safari/i.test(userAgent) && !/Chrome/i.test(userAgent)) return 'Safari' + if (/Firefox/i.test(userAgent)) return 'Firefox' + if (/Edg/i.test(userAgent)) return 'Edge' + return 'Other' +} + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'POST') { + return res.status(405).json({ error: 'Method not allowed' }) + } + + const webhookUrl = process.env.SLACK_WEBHOOK_URL + + if (!webhookUrl) { + return res.status(500).json({ error: 'Slack webhook URL not configured' }) + } + + try { + const { slideDocs, totalPages, userAgent, startTime, endTime, duration } = + req.body + + // IPアドレスを取得(Vercel/プロキシ対応) + const forwarded = req.headers['x-forwarded-for'] + const ip = + (typeof forwarded === 'string' + ? forwarded.split(',')[0] + : forwarded?.[0]) || + req.socket?.remoteAddress || + 'Unknown' + + // デバイス・ブラウザ情報 + const deviceType = getDeviceType(userAgent || '') + const browser = getBrowser(userAgent || '') + const isMobile = isMobileDevice(userAgent || '') + + const message = { + text: `🎉 プレゼンテーション完了通知`, + blocks: [ + { + type: 'header', + text: { + type: 'plain_text', + text: '🎉 プレゼンテーション完了', + emoji: true, + }, + }, + { + type: 'section', + fields: [ + { + type: 'mrkdwn', + text: `*スライド:*\n${slideDocs}`, + }, + { + type: 'mrkdwn', + text: `*総ページ数:*\n${totalPages}ページ`, + }, + ], + }, + { + type: 'section', + fields: [ + { + type: 'mrkdwn', + text: `*開始時刻:*\n${startTime}`, + }, + { + type: 'mrkdwn', + text: `*終了時刻:*\n${endTime}`, + }, + ], + }, + { + type: 'section', + fields: [ + { + type: 'mrkdwn', + text: `*視聴時間:*\n⏱️ ${duration}`, + }, + { + type: 'mrkdwn', + text: `*デバイス:*\n${deviceType}`, + }, + ], + }, + { + type: 'section', + fields: [ + { + type: 'mrkdwn', + text: `*ブラウザ:*\n${browser}`, + }, + { + type: 'mrkdwn', + text: `*IPアドレス:*\n\`${ip}\``, + }, + ], + }, + { + type: 'context', + elements: [ + { + type: 'mrkdwn', + text: `_${isMobile ? '📱 モバイル' : '💻 デスクトップ'} | ${userAgent?.substring(0, 80) || 'Unknown'}..._`, + }, + ], + }, + ], + } + + const response = await fetch(webhookUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(message), + }) + + if (!response.ok) { + throw new Error(`Slack API error: ${response.status}`) + } + + res.status(200).json({ success: true }) + } catch (error) { + console.error('Slack notification error:', error) + res.status(500).json({ error: 'Failed to send notification' }) + } +} diff --git a/src/pages/api/tts-aicu.ts b/src/pages/api/tts-aicu.ts new file mode 100644 index 000000000..a3f23a0ba --- /dev/null +++ b/src/pages/api/tts-aicu.ts @@ -0,0 +1,50 @@ +import type { NextApiRequest, NextApiResponse } from 'next' + +type Data = { + audio?: string // Base64 encoded string + error?: string +} + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + const message = req.body.message + const slug = req.body.slug || 'luc4' + + const apiKey = process.env.AICU_API_KEY + + if (!apiKey) { + res.status(500).json({ error: 'AICU_API_KEY is not configured' }) + return + } + + try { + const response = await fetch('https://api.aicu.ai/api/v1/tts/generate', { + method: 'POST', + headers: { + Authorization: `Bearer ${apiKey}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + text: message, + slug: slug, + format: 'mp3', + }), + }) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(`AICU API error ${response.status}: ${errorText}`) + } + + // Get audio as ArrayBuffer and convert to Base64 + const audioBuffer = await response.arrayBuffer() + const audioContent = Buffer.from(audioBuffer).toString('base64') + + res.status(200).json({ audio: audioContent }) + } catch (error) { + console.error('Error in AICU Text-to-Speech:', error) + res.status(500).json({ error: 'Internal Server Error' }) + } +}