mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-05-17 22:33:06 +08:00
docs: fix zh-CN parity — add 44 missing files to ja-JP
Add files present in zh-CN but missing from ja-JP: - commands: claw, context-budget, devfleet, docs, projects, prompt-optimize, rules-distill (7 files) - skills: regex-vs-llm-structured-text, remotion-video-creation, repo-scan, research-ops, returns-reverse-logistics, rules-distill, rust-patterns, rust-testing, skill-comply, skill-stocktake, social-graph-ranker, swift-actor-persistence, swift-concurrency-6-2, swift-protocol-di-testing, swiftui-patterns, team-builder, terminal-ops, token-budget-advisor, ui-demo, unified-notifications-ops, video-editing, videodb (+reference/*), visa-doc-translate, workspace-surface-audit, x-api (37 files) Result: ja-JP now has 517 files vs zh-CN 412 files. zh-CN parity: 0 missing files (complete parity achieved).
This commit is contained in:
51
docs/ja-JP/commands/claw.md
Normal file
51
docs/ja-JP/commands/claw.md
Normal file
@@ -0,0 +1,51 @@
|
||||
---
|
||||
description: NanoClaw v2 を起動します — モデルルーティング、スキルホットロード、ブランチ、圧縮、エクスポート、メトリクス機能を備えた ECC の永続的でゼロ依存の REPL。
|
||||
---
|
||||
|
||||
# Claw コマンド
|
||||
|
||||
永続的な Markdown 履歴と操作コントロールを備えた、インタラクティブな AI エージェントセッションを起動します。
|
||||
|
||||
## 使用方法
|
||||
|
||||
```bash
|
||||
node scripts/claw.js
|
||||
```
|
||||
|
||||
または npm 経由:
|
||||
|
||||
```bash
|
||||
npm run claw
|
||||
```
|
||||
|
||||
## 環境変数
|
||||
|
||||
| 変数 | デフォルト値 | 説明 |
|
||||
|----------|---------|-------------|
|
||||
| `CLAW_SESSION` | `default` | セッション名(英数字 + ハイフン) |
|
||||
| `CLAW_SKILLS` | *(空)* | 起動時に読み込むスキルのカンマ区切りリスト |
|
||||
| `CLAW_MODEL` | `sonnet` | セッションのデフォルトモデル |
|
||||
|
||||
## REPL コマンド
|
||||
|
||||
```text
|
||||
/help ヘルプを表示
|
||||
/clear 現在のセッション履歴をクリア
|
||||
/history 会話履歴全体を表示
|
||||
/sessions 保存済みセッションを一覧表示
|
||||
/model [name] モデルを表示/設定
|
||||
/load <skill-name> スキルをコンテキストにホットロード
|
||||
/branch <session-name> 現在のセッションをブランチ
|
||||
/search <query> セッションをまたいでクエリを検索
|
||||
/compact 古いラウンドを圧縮し、最近のコンテキストを保持
|
||||
/export <md|json|txt> [path] セッションをエクスポート
|
||||
/metrics セッションメトリクスを表示
|
||||
exit 終了
|
||||
```
|
||||
|
||||
## 説明
|
||||
|
||||
* NanoClaw はゼロ依存を維持します。
|
||||
* セッションは `~/.claude/claw/<session>.md` に保存されます。
|
||||
* 圧縮は最近のラウンドを保持し、圧縮ヘッダーを書き込みます。
|
||||
* エクスポートは Markdown、JSON ラウンド、プレーンテキストに対応しています。
|
||||
29
docs/ja-JP/commands/context-budget.md
Normal file
29
docs/ja-JP/commands/context-budget.md
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
description: エージェント、スキル、MCP サーバー、ルールにわたるコンテキストウィンドウの使用状況を分析し、最適化の機会を探ります。トークンオーバーヘッドの削減とパフォーマンス警告の回避に役立ちます。
|
||||
---
|
||||
|
||||
# コンテキストバジェット最適化ツール
|
||||
|
||||
Claude Code の設定におけるコンテキストウィンドウの消費量を分析し、トークンオーバーヘッドを削減するための実用的な推奨事項を提供します。
|
||||
|
||||
## 使用方法
|
||||
|
||||
```
|
||||
/context-budget [--verbose]
|
||||
```
|
||||
|
||||
* デフォルト:サマリーと主要な推奨事項を提供
|
||||
* `--verbose`:コンポーネントごとの完全な内訳を提供
|
||||
|
||||
$ARGUMENTS
|
||||
|
||||
## 操作手順
|
||||
|
||||
**context-budget** スキル(`skills/context-budget/SKILL.md`)を実行し、以下を入力します:
|
||||
|
||||
1. `$ARGUMENTS` に `--verbose` フラグが存在する場合、そのフラグを渡す
|
||||
2. ユーザーが別途指定しない限り、200K コンテキストウィンドウ(Claude Sonnet のデフォルト)を仮定する
|
||||
3. スキルの4フェーズに従う:インベントリ → 分類 → 問題検出 → レポート
|
||||
4. フォーマット済みのコンテキストバジェットレポートをユーザーに出力する
|
||||
|
||||
すべてのスキャンロジック、トークン推定、問題検出、レポートフォーマットはスキルが担当します。
|
||||
93
docs/ja-JP/commands/devfleet.md
Normal file
93
docs/ja-JP/commands/devfleet.md
Normal file
@@ -0,0 +1,93 @@
|
||||
---
|
||||
description: Claude DevFleet を使って並列 Claude Code エージェントをオーケストレーションします — 自然言語でプロジェクトを計画し、隔離されたワークツリーにエージェントをディスパッチし、進捗を監視し、構造化レポートを読み取ります。
|
||||
---
|
||||
|
||||
# DevFleet — マルチエージェントオーケストレーション
|
||||
|
||||
Claude DevFleet を使って並列の Claude Code エージェントをオーケストレーションします。各エージェントは隔離された git worktree 内で動作し、完全なツールチェーンを備えています。
|
||||
|
||||
DevFleet MCP サーバーが必要です:`claude mcp add devfleet --transport http http://localhost:18801/mcp`
|
||||
|
||||
## フロー
|
||||
|
||||
```
|
||||
ユーザーがプロジェクトを説明
|
||||
→ plan_project(prompt) → タスク DAG と依存関係
|
||||
→ 計画を表示し、承認を取得
|
||||
→ dispatch_mission(M1) → エージェントがワークスペースで生成
|
||||
→ M1 完了 → 自動マージ → M2 が自動ディスパッチ(M1 に依存)
|
||||
→ M2 完了 → 自動マージ
|
||||
→ get_report(M2) → ファイル変更、完了内容、エラー、次のステップ
|
||||
→ ユーザーにサマリーをレポート
|
||||
```
|
||||
|
||||
## ワークフロー
|
||||
|
||||
1. **ユーザーの説明に基づいてプロジェクトを計画する**:
|
||||
|
||||
```
|
||||
mcp__devfleet__plan_project(prompt="<ユーザーの説明>")
|
||||
```
|
||||
|
||||
これはチェーン状のタスクを含むプロジェクトを返します。ユーザーに以下を表示します:
|
||||
|
||||
* プロジェクト名と ID
|
||||
* 各タスク:タイトル、タイプ、依存関係
|
||||
* 依存関係 DAG(どのタスクがどのタスクをブロックしているか)
|
||||
|
||||
2. **ディスパッチ前にユーザーの承認を待つ**。計画を明確に表示します。
|
||||
|
||||
3. **最初のタスクをディスパッチする**(`depends_on` が空のタスク):
|
||||
|
||||
```
|
||||
mcp__devfleet__dispatch_mission(mission_id="<first_mission_id>")
|
||||
```
|
||||
|
||||
残りのタスクは依存関係が完了すると自動的にディスパッチされます(`plan_project` が `auto_dispatch=true` でタスクを作成するため)。`create_mission` を使ってタスクを手動作成する場合は、この動作を有効にするために `auto_dispatch=true` を明示的に設定する必要があります。
|
||||
|
||||
4. **進捗を監視する** — 実行中の内容を確認:
|
||||
|
||||
```
|
||||
mcp__devfleet__get_dashboard()
|
||||
```
|
||||
|
||||
または特定のタスクを確認:
|
||||
|
||||
```
|
||||
mcp__devfleet__get_mission_status(mission_id="<id>")
|
||||
```
|
||||
|
||||
長時間実行するタスクには、`wait_for_mission` ではなく `get_mission_status` によるポーリングを優先し、ユーザーが進捗の更新を確認できるようにします。
|
||||
|
||||
5. **完了した各タスクのレポートを読む**:
|
||||
|
||||
```
|
||||
mcp__devfleet__get_report(mission_id="<mission_id>")
|
||||
```
|
||||
|
||||
終了状態に達した各タスクに対してこのツールを呼び出します。レポートには files\_changed、what\_done、what\_open、what\_tested、what\_untested、next\_steps、errors\_encountered が含まれます。
|
||||
|
||||
## 利用可能なすべてのツール
|
||||
|
||||
| ツール | 用途 |
|
||||
|------|---------|
|
||||
| `plan_project(prompt)` | AI が説明を `auto_dispatch=true` のチェーン状タスクに分解する |
|
||||
| `create_project(name, path?, description?)` | プロジェクトを手動作成し、`project_id` を返す |
|
||||
| `create_mission(project_id, title, prompt, depends_on?, auto_dispatch?)` | タスクを追加する。`depends_on` はタスク ID 文字列のリスト |
|
||||
| `dispatch_mission(mission_id, model?, max_turns?)` | エージェントを起動する |
|
||||
| `cancel_mission(mission_id)` | 実行中のエージェントを停止する |
|
||||
| `wait_for_mission(mission_id, timeout_seconds?)` | 完了までブロックする(長いタスクにはポーリングを優先) |
|
||||
| `get_mission_status(mission_id)` | ノンブロッキングで進捗を確認する |
|
||||
| `get_report(mission_id)` | 構造化レポートを読む |
|
||||
| `get_dashboard()` | システム概要 |
|
||||
| `list_projects()` | プロジェクトを参照する |
|
||||
| `list_missions(project_id, status?)` | タスクを一覧表示する |
|
||||
|
||||
## ガイドライン
|
||||
|
||||
* ユーザーが明示的に「始めてください」と言わない限り、ディスパッチ前に必ず計画を確認する
|
||||
* ステータスをレポートする際はタスクのタイトルと ID を含める
|
||||
* タスクが失敗した場合、再試行前にそのレポートを読んでエラーを把握する
|
||||
* エージェントの同時実行数は設定可能(デフォルト:3)。超過したタスクはキューに入れられ、スロットが空くと自動的にディスパッチされる。スロットの可用性は `get_dashboard()` で確認する
|
||||
* 依存関係は DAG を形成する — 循環依存は絶対に作成しない
|
||||
* 各エージェントは完了時に自動的に worktree をマージする。マージ競合が発生した場合、変更は手動解決のために worktree ブランチに保持される
|
||||
32
docs/ja-JP/commands/docs.md
Normal file
32
docs/ja-JP/commands/docs.md
Normal file
@@ -0,0 +1,32 @@
|
||||
---
|
||||
description: Context7 を使ってライブラリやトピックの最新ドキュメントを検索します。
|
||||
---
|
||||
|
||||
# /docs
|
||||
|
||||
## 目的
|
||||
|
||||
ライブラリ、フレームワーク、または API の最新ドキュメントを検索し、関連するコードスニペットを含む要約された回答を返します。Context7 MCP(resolve-library-id と query-docs)を使用するため、回答はトレーニングデータではなく最新のドキュメントを反映しています。
|
||||
|
||||
## 使い方
|
||||
|
||||
```
|
||||
/docs [library name] [question]
|
||||
```
|
||||
|
||||
複数の単語からなる引数には、単一のトークンとして解析されるよう引用符を使用してください。例:`/docs "Next.js" "How do I configure middleware?"`
|
||||
|
||||
ライブラリまたは質問が省略された場合、ユーザーに入力を求めます:
|
||||
|
||||
1. ライブラリまたは製品名(例:Next.js、Prisma、Supabase)。
|
||||
2. 具体的な質問またはタスク(例:「ミドルウェアの設定方法は?」、「認証方法」)。
|
||||
|
||||
## ワークフロー
|
||||
|
||||
1. **ライブラリ ID を解決する** — Context7 ツール `resolve-library-id` をライブラリ名とユーザーの質問とともに呼び出し、Context7 互換のライブラリ ID(例:`/vercel/next.js`)を取得する。
|
||||
2. **ドキュメントをクエリする** — そのライブラリ ID とユーザーの質問を使って `query-docs` を呼び出す。
|
||||
3. **要約する** — 簡潔な回答を返し、取得したドキュメントから抽出した関連コード例を含める。ライブラリ(関連する場合はバージョンも含めて)に言及する。
|
||||
|
||||
## 出力
|
||||
|
||||
ユーザーは、最新のドキュメントに基づいた簡潔で正確な回答と、役立つコードスニペットを受け取ります。Context7 が利用できない場合は、その旨を説明し、トレーニングデータに基づいて回答しますが、ドキュメントが古い可能性があることを注記します。
|
||||
39
docs/ja-JP/commands/projects.md
Normal file
39
docs/ja-JP/commands/projects.md
Normal file
@@ -0,0 +1,39 @@
|
||||
---
|
||||
name: projects
|
||||
description: 既知のプロジェクトとその本能統計を一覧表示する
|
||||
command: true
|
||||
---
|
||||
|
||||
# プロジェクト コマンド
|
||||
|
||||
continuous-learning-v2 のプロジェクト登録エントリと各プロジェクトの本能/観察カウントを一覧表示します。
|
||||
|
||||
## 実装
|
||||
|
||||
プラグインルートパスを使って本能 CLI を実行します:
|
||||
|
||||
```bash
|
||||
python3 "${CLAUDE_PLUGIN_ROOT}/skills/continuous-learning-v2/scripts/instinct-cli.py" projects
|
||||
```
|
||||
|
||||
または `CLAUDE_PLUGIN_ROOT` が設定されていない場合(手動インストール):
|
||||
|
||||
```bash
|
||||
python3 ~/.claude/skills/continuous-learning-v2/scripts/instinct-cli.py projects
|
||||
```
|
||||
|
||||
## 使い方
|
||||
|
||||
```bash
|
||||
/projects
|
||||
```
|
||||
|
||||
## 操作手順
|
||||
|
||||
1. `~/.claude/homunculus/projects.json` を読み取る
|
||||
2. 各プロジェクトについて以下を表示する:
|
||||
* プロジェクト名、ID、ルートディレクトリ、リモートアドレス
|
||||
* 個人および継承された本能カウント
|
||||
* 観察イベントカウント
|
||||
* 最終確認タイムスタンプ
|
||||
3. グローバル本能の合計も表示する
|
||||
37
docs/ja-JP/commands/prompt-optimize.md
Normal file
37
docs/ja-JP/commands/prompt-optimize.md
Normal file
@@ -0,0 +1,37 @@
|
||||
---
|
||||
description: ドラフトプロンプトを分析し、ECC が強化された最適化済みバージョンを出力します。貼り付けてすぐに実行できる状態で出力されます。タスクは実行しません — コンサルティング分析のみを出力します。
|
||||
---
|
||||
|
||||
# /prompt-optimize
|
||||
|
||||
以下のプロンプトを分析し、ECC レバレッジを最大化するよう最適化します。
|
||||
|
||||
## あなたのタスク
|
||||
|
||||
ユーザーの入力に **prompt-optimizer** スキルを適用します。6フェーズの分析プロセスに従ってください:
|
||||
|
||||
0. **プロジェクト検出** — CLAUDE.md を読み取り、プロジェクトファイル(package.json、go.mod、pyproject.toml など)から技術スタックを検出する
|
||||
1. **意図検出** — タスクタイプを分類する(新機能、バグ修正、リファクタリング、調査、テスト、レビュー、ドキュメント、インフラ、設計)
|
||||
2. **スコープ評価** — 複雑さを評価する(シンプル / 低 / 中 / 高 / エピック)、コードベースが検出された場合はそのサイズをシグナルとして使用する
|
||||
3. **ECC コンポーネントマッチング** — 特定のスキル、コマンド、エージェント、モデル階層にマッピングする
|
||||
4. **不足コンテキスト検出** — 情報のギャップを特定する。3つ以上の重要な項目が不足している場合は、生成前にユーザーに確認を求める
|
||||
5. **ワークフローとモデル** — ライフサイクルフェーズを決定し、モデル階層を推奨し、複雑さが高/エピックの場合は複数のプロンプトに分割する
|
||||
|
||||
## 出力要件
|
||||
|
||||
* 診断結果、推奨 ECC コンポーネント、prompt-optimizer スキルの出力フォーマットを使用した最適化済みプロンプトを提示する
|
||||
* **完全版**(詳細)と**クイック版**(コンパクト、意図タイプによって変化)を提供する
|
||||
* ユーザーの入力と同じ言語で回答する
|
||||
* 最適化済みプロンプトは完全で、新しいセッションにコピー&ペーストしてそのまま使用できる状態でなければならない
|
||||
* 調整オプションまたは明確な次のアクション(別途実行リクエストを開始するため)を提供するフッターで締めくくる
|
||||
|
||||
## 重要
|
||||
|
||||
ユーザーのタスクを実行しないでください。分析結果と最適化済みプロンプトのみを出力してください。
|
||||
ユーザーが直接実行を求めた場合は、`/prompt-optimize` はコンサルティング的な出力のみを生成することを説明し、通常のタスクリクエストを開始するよう伝えてください。
|
||||
|
||||
注意:`blueprint` は**スキル**であり、スラッシュコマンドではありません。「ブループリントスキルを使用する」と書き、`/...` コマンドとして表現しないでください。
|
||||
|
||||
## ユーザー入力
|
||||
|
||||
$ARGUMENTS
|
||||
11
docs/ja-JP/commands/rules-distill.md
Normal file
11
docs/ja-JP/commands/rules-distill.md
Normal file
@@ -0,0 +1,11 @@
|
||||
---
|
||||
description: "スキルをスキャンして横断的な原則を抽出し、ルールとして提炼する"
|
||||
---
|
||||
|
||||
# /rules-distill — スキルから原則をルールとして提炼する
|
||||
|
||||
インストール済みのスキルをスキャンし、横断的な原則を抽出して、ルールとして提炼します。
|
||||
|
||||
## フロー
|
||||
|
||||
`rules-distill` スキルで定義された完全なワークフローに従います。
|
||||
333
docs/ja-JP/skills/frontend-slides/STYLE_PRESETS.md
Normal file
333
docs/ja-JP/skills/frontend-slides/STYLE_PRESETS.md
Normal file
@@ -0,0 +1,333 @@
|
||||
# スタイルプリセットリファレンス
|
||||
|
||||
`frontend-slides` 用にまとめられたビジュアルスタイル。
|
||||
|
||||
このファイルの用途:
|
||||
|
||||
* 強制的なビューポート適合CSSの基礎
|
||||
* プリセットの選択とムードマッピング
|
||||
* CSSの落とし穴とバリデーションルール
|
||||
|
||||
抽象的な形状のみを使用する。ユーザーが明示的に要求しない限り、イラストを避ける。
|
||||
|
||||
## ビューポート適合は妥協しない
|
||||
|
||||
各スライドは1つのビューポートに完全に収まる必要がある。
|
||||
|
||||
### 黄金ルール
|
||||
|
||||
```text
|
||||
各スライド = ちょうど1つのビューポートの高さ。
|
||||
コンテンツが多すぎる = 複数のスライドに分割する。
|
||||
スライド内でスクロールさせない。
|
||||
```
|
||||
|
||||
### コンテンツ密度の制限
|
||||
|
||||
| スライドタイプ | 最大コンテンツ量 |
|
||||
|---|---|
|
||||
| タイトルスライド | 1つのタイトル + 1つのサブタイトル + オプションのキャッチフレーズ |
|
||||
| コンテンツスライド | 1つのタイトル + 4〜6つの箇条書きまたは2段落 |
|
||||
| 機能グリッド | 最大6枚のカード |
|
||||
| コードスライド | 最大8〜10行 |
|
||||
| 引用スライド | 1つの引用 + 出典 |
|
||||
| 画像スライド | 1枚の画像、理想的には60vh未満 |
|
||||
|
||||
## 強制基礎CSS
|
||||
|
||||
このコードブロックを生成されるすべてのプレゼンテーションにコピーし、その上にテーマを適用する。
|
||||
|
||||
```css
|
||||
/* ===========================================
|
||||
VIEWPORT FITTING: MANDATORY BASE STYLES
|
||||
=========================================== */
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-snap-type: y mandatory;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
.slide {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
overflow: hidden;
|
||||
scroll-snap-align: start;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.slide-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
max-height: 100%;
|
||||
overflow: hidden;
|
||||
padding: var(--slide-padding);
|
||||
}
|
||||
|
||||
:root {
|
||||
--title-size: clamp(1.5rem, 5vw, 4rem);
|
||||
--h2-size: clamp(1.25rem, 3.5vw, 2.5rem);
|
||||
--h3-size: clamp(1rem, 2.5vw, 1.75rem);
|
||||
--body-size: clamp(0.75rem, 1.5vw, 1.125rem);
|
||||
--small-size: clamp(0.65rem, 1vw, 0.875rem);
|
||||
|
||||
--slide-padding: clamp(1rem, 4vw, 4rem);
|
||||
--content-gap: clamp(0.5rem, 2vw, 2rem);
|
||||
--element-gap: clamp(0.25rem, 1vw, 1rem);
|
||||
}
|
||||
|
||||
.card, .container, .content-box {
|
||||
max-width: min(90vw, 1000px);
|
||||
max-height: min(80vh, 700px);
|
||||
}
|
||||
|
||||
.feature-list, .bullet-list {
|
||||
gap: clamp(0.4rem, 1vh, 1rem);
|
||||
}
|
||||
|
||||
.feature-list li, .bullet-list li {
|
||||
font-size: var(--body-size);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 250px), 1fr));
|
||||
gap: clamp(0.5rem, 1.5vw, 1rem);
|
||||
}
|
||||
|
||||
img, .image-container {
|
||||
max-width: 100%;
|
||||
max-height: min(50vh, 400px);
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
@media (max-height: 700px) {
|
||||
:root {
|
||||
--slide-padding: clamp(0.75rem, 3vw, 2rem);
|
||||
--content-gap: clamp(0.4rem, 1.5vw, 1rem);
|
||||
--title-size: clamp(1.25rem, 4.5vw, 2.5rem);
|
||||
--h2-size: clamp(1rem, 3vw, 1.75rem);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-height: 600px) {
|
||||
:root {
|
||||
--slide-padding: clamp(0.5rem, 2.5vw, 1.5rem);
|
||||
--content-gap: clamp(0.3rem, 1vw, 0.75rem);
|
||||
--title-size: clamp(1.1rem, 4vw, 2rem);
|
||||
--body-size: clamp(0.7rem, 1.2vw, 0.95rem);
|
||||
}
|
||||
|
||||
.nav-dots, .keyboard-hint, .decorative {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-height: 500px) {
|
||||
:root {
|
||||
--slide-padding: clamp(0.4rem, 2vw, 1rem);
|
||||
--title-size: clamp(1rem, 3.5vw, 1.5rem);
|
||||
--h2-size: clamp(0.9rem, 2.5vw, 1.25rem);
|
||||
--body-size: clamp(0.65rem, 1vw, 0.85rem);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
:root {
|
||||
--title-size: clamp(1.25rem, 7vw, 2.5rem);
|
||||
}
|
||||
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, *::before, *::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
transition-duration: 0.2s !important;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: auto;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## ビューポートチェックリスト
|
||||
|
||||
* すべての `.slide` に `height: 100vh`、`height: 100dvh`、`overflow: hidden` がある
|
||||
* すべてのタイポグラフィが `clamp()` を使用している
|
||||
* すべての間隔が `clamp()` またはビューポート単位を使用している
|
||||
* 画像に `max-height` 制約がある
|
||||
* グリッドが適合のために `auto-fit` + `minmax()` を使用している
|
||||
* 短い高さのブレークポイントが `700px`、`600px`、`500px` に存在する
|
||||
* コンテンツが窮屈に感じられる場合は、スライドを分割する
|
||||
|
||||
## ムードからプリセットへのマッピング
|
||||
|
||||
| ムード | 推奨プリセット |
|
||||
|---|---|
|
||||
| 印象的 / 自信あり | Bold Signal, Electric Studio, Dark Botanical |
|
||||
| 興奮 / 活力 | Creative Voltage, Neon Cyber, Split Pastel |
|
||||
| 落ち着き / 集中 | Notebook Tabs, Paper & Ink, Swiss Modern |
|
||||
| インスピレーション / 感動 | Dark Botanical, Vintage Editorial, Pastel Geometry |
|
||||
|
||||
## プリセットカタログ
|
||||
|
||||
### 1. Bold Signal
|
||||
|
||||
* 雰囲気:自信あり、高インパクト、基調講演に適している
|
||||
* 最適用途:ピッチデッキ、製品ローンチ、アナウンス
|
||||
* フォント:Archivo Black + Space Grotesk
|
||||
* カラーパレット:チャコールの基調色、明るいオレンジのフォーカスカード、純白のテキスト
|
||||
* 特徴:超大きなセクション番号、ダーク背景上の高コントラストカード
|
||||
|
||||
### 2. Electric Studio
|
||||
|
||||
* 雰囲気:クリーン、大胆、機関誌レベルの洗練さ
|
||||
* 最適用途:クライアントデッキ、戦略レビュー
|
||||
* フォント:Manropeのみ
|
||||
* カラーパレット:ブラック、ホワイト、彩度の高いコバルトブルーのアクセント
|
||||
* 特徴:デュアルパネル分割とシャープな編集スタイルのアライメント
|
||||
|
||||
### 3. Creative Voltage
|
||||
|
||||
* 雰囲気:活力、レトロモダン、遊び心と自信
|
||||
* 最適用途:クリエイティブスタジオ、ブランドワーク、プロダクトストーリーテリング
|
||||
* フォント:Syne + Space Mono
|
||||
* カラーパレット:エレクトリックブルー、ネオンイエロー、ディープネイビー
|
||||
* 特徴:ハーフトーンテクスチャ、バッジ、強いコントラスト
|
||||
|
||||
### 4. Dark Botanical
|
||||
|
||||
* 雰囲気:エレガント、ハイエンド、雰囲気がある
|
||||
* 最適用途:ラグジュアリーブランド、思慮深いナラティブ、プレミアム製品デモ
|
||||
* フォント:Cormorant + IBM Plex Sans
|
||||
* カラーパレット:ほぼブラック、温かみのあるアイボリー、ブラッシュ、ゴールド、テラコッタ
|
||||
* 特徴:ぼかされた抽象的な円、細いライン、抑制されたモーション
|
||||
|
||||
### 5. Notebook Tabs
|
||||
|
||||
* 雰囲気:編集的、整理された、触覚的
|
||||
* 最適用途:レポート、レビュー、構造化されたストーリーテリング
|
||||
* フォント:Bodoni Moda + DM Sans
|
||||
* カラーパレット:チャコール上のクリーム色の用紙とソフトカラーのタブ
|
||||
* 特徴:紙の効果、カラーサイドタブ、バインダーの詳細
|
||||
|
||||
### 6. Pastel Geometry
|
||||
|
||||
* 雰囲気:親しみやすい、モダン、フレンドリー
|
||||
* 最適用途:製品概要、入門、軽めのブランドプレゼン
|
||||
* フォント:Plus Jakarta Sansのみ
|
||||
* カラーパレット:薄いブルーの背景、クリーム色のカード、ソフトなピンク/ミント/ラベンダーのアクセント
|
||||
* 特徴:縦長のピル形状、角丸カード、ソフトシャドウ
|
||||
|
||||
### 7. Split Pastel
|
||||
|
||||
* 雰囲気:楽しい、モダン、クリエイティブ
|
||||
* 最適用途:エージェンシー紹介、ワークショップ、ポートフォリオ
|
||||
* フォント:Outfitのみ
|
||||
* カラーパレット:ミントバッジとのピーチ + ラベンダーの分割背景
|
||||
* 特徴:分割背景、角丸タグ、軽いグリッドオーバーレイ
|
||||
|
||||
### 8. Vintage Editorial
|
||||
|
||||
* 雰囲気:機知に富む、個性的、雑誌にインスパイアされた
|
||||
* 最適用途:パーソナルブランド、オピニオントーク、ストーリーテリング
|
||||
* フォント:Fraunces + Work Sans
|
||||
* カラーパレット:クリーム、チャコール、くすんだ温かみのあるアクセント
|
||||
* 特徴:幾何学的なアクセント、ボーダー付きのコールアウト、印象的なセリフの見出し
|
||||
|
||||
### 9. Neon Cyber
|
||||
|
||||
* 雰囲気:未来的、テック感、ダイナミック
|
||||
* 最適用途:AI、インフラ、デベロッパーツール、未来トレンドについての講演
|
||||
* フォント:Clash Display + Satoshi
|
||||
* カラーパレット:ミッドナイトネイビー、シアン、マゼンタ
|
||||
* 特徴:グロー効果、パーティクル、グリッド、データレーダーエナジー感
|
||||
|
||||
### 10. Terminal Green
|
||||
|
||||
* 雰囲気:デベロッパー向け、ハッカーな簡潔さ
|
||||
* 最適用途:API、CLIツール、エンジニアリングデモ
|
||||
* フォント:JetBrains Monoのみ
|
||||
* カラーパレット:GitHubダーク + ターミナルグリーン
|
||||
* 特徴:スキャンライン、コマンドラインフレーミング、精確なモノスペースのリズム
|
||||
|
||||
### 11. Swiss Modern
|
||||
|
||||
* 雰囲気:ミニマリスト、精密、データ指向
|
||||
* 最適用途:エンタープライズ、製品戦略、アナリティクス
|
||||
* フォント:Archivo + Nunito
|
||||
* カラーパレット:ホワイト、ブラック、シグナルレッド
|
||||
* 特徴:可視グリッド、非対称、幾何学的な秩序感
|
||||
|
||||
### 12. Paper & Ink
|
||||
|
||||
* 雰囲気:文学的、思慮深い、ストーリー駆動
|
||||
* 最適用途:散文、基調講演のナラティブ、マニフェスト的なプレゼン
|
||||
* フォント:Cormorant Garamond + Source Serif 4
|
||||
* カラーパレット:温かみのあるクリーム、チャコール、ディープレッドのアクセント
|
||||
* 特徴:引用のハイライト、ドロップキャップ、エレガントなライン
|
||||
|
||||
## 直接選択プロンプト
|
||||
|
||||
ユーザーがすでに望むスタイルを知っている場合、プレビューを強制的に生成するのではなく、上記のプリセット名から直接選んでもらう。
|
||||
|
||||
## アニメーションの感覚マッピング
|
||||
|
||||
| 感覚 | モーションの方向 |
|
||||
|---|---|
|
||||
| ドラマチック / シネマティック | ゆっくりとしたフェード、視差スクロール、大スケールのズームイン |
|
||||
| テック感 / 未来的 | グロー、パーティクル、グリッドモーション、テキストのスクランブル表示 |
|
||||
| 楽しい / フレンドリー | バウンスのイージング、丸い形状、フローティングモーション |
|
||||
| プロフェッショナル / エンタープライズ | 微妙な200〜300msのトランジション、クリーンなスライド切り替え |
|
||||
| 落ち着き / ミニマリスト | 非常に控えめなモーション、空白を優先 |
|
||||
| 編集的 / 雑誌的 | 強い階層性、テキストと画像のずらしたインタラクション |
|
||||
|
||||
## CSSの落とし穴:否定関数
|
||||
|
||||
以下は絶対に書かない:
|
||||
|
||||
```css
|
||||
right: -clamp(28px, 3.5vw, 44px);
|
||||
margin-left: -min(10vw, 100px);
|
||||
```
|
||||
|
||||
ブラウザはそれらを静かに無視する。
|
||||
|
||||
代わりに常にこのように書く:
|
||||
|
||||
```css
|
||||
right: calc(-1 * clamp(28px, 3.5vw, 44px));
|
||||
margin-left: calc(-1 * min(10vw, 100px));
|
||||
```
|
||||
|
||||
## バリデーションサイズ
|
||||
|
||||
少なくとも以下のサイズでテストする:
|
||||
|
||||
* デスクトップ:`1920x1080`、`1440x900`、`1280x720`
|
||||
* タブレット:`1024x768`、`768x1024`
|
||||
* モバイル:`375x667`、`414x896`
|
||||
* 横向きモバイル:`667x375`、`896x414`
|
||||
|
||||
## アンチパターン
|
||||
|
||||
使用しない:
|
||||
|
||||
* 紫背景に白テキストのスタートアップテンプレート
|
||||
* Inter / Roboto / Arial をビジュアルボイスとして使用する(ユーザーが実用主義的なニュートラルスタイルを明示的に望む場合を除く)
|
||||
* 箇条書きの詰め込み、過小なフォント、スクロールが必要なコードブロック
|
||||
* 抽象的な幾何学形状がより良い働きをする場合に装飾的なイラストを使用する
|
||||
217
docs/ja-JP/skills/regex-vs-llm-structured-text/SKILL.md
Normal file
217
docs/ja-JP/skills/regex-vs-llm-structured-text/SKILL.md
Normal file
@@ -0,0 +1,217 @@
|
||||
---
|
||||
name: regex-vs-llm-structured-text
|
||||
description: 構造化テキストの解析に正規表現と大規模言語モデルのどちらを使うかを選択するための意思決定フレームワーク——まず正規表達式から始め、信頼度の低いエッジケースにのみ大規模言語モデルを追加する。
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# 構造化テキスト解析における正規表現 vs LLM
|
||||
|
||||
構造化テキスト(クイズ、フォーム、請求書、ドキュメント)を解析するための実用的な意思決定フレームワーク。核心的な洞察:正規表現は低コストかつ決定論的に95〜98%のケースを処理できる。コストのかかるLLM呼び出しは残りのエッジケースに留める。
|
||||
|
||||
## 使用場面
|
||||
|
||||
* 繰り返しパターンを持つ構造化テキスト(設問、フォーム、表)の解析
|
||||
* テキスト抽出に正規表現とLLMのどちらを使うかの判断
|
||||
* 両方のアプローチを組み合わせたハイブリッドパイプラインの構築
|
||||
* テキスト処理におけるコスト/精度のトレードオフの最適化
|
||||
|
||||
## 意思決定フレームワーク
|
||||
|
||||
```
|
||||
テキスト形式は一貫していて繰り返しがあるか?
|
||||
├── はい (>90% が何らかのパターンに従う) → 正規表現から始める
|
||||
│ ├── 正規表現が 95%+ を処理 → 完了、LLM は不要
|
||||
│ └── 正規表現が <95% を処理 → エッジケースのみ LLM を追加
|
||||
└── いいえ (自由形式、高度に可変) → LLM を直接使用
|
||||
```
|
||||
|
||||
## アーキテクチャパターン
|
||||
|
||||
```
|
||||
[正規表現パーサー] ─── 構造を抽出(95〜98% の精度)
|
||||
│
|
||||
▼
|
||||
[テキストクリーナー] ─── ノイズを除去(マーカー、ページ番号、アーティファクト)
|
||||
│
|
||||
▼
|
||||
[信頼度スコアラー] ─── 信頼度の低い抽出結果にフラグを立てる
|
||||
│
|
||||
├── 高信頼度(≥0.95)→ 直接出力
|
||||
│
|
||||
└── 低信頼度(<0.95)→ [LLM バリデーター] → 出力
|
||||
```
|
||||
|
||||
## 実装
|
||||
|
||||
### 1. 正規表現パーサー(大半のケースを処理)
|
||||
|
||||
```python
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ParsedItem:
|
||||
id: str
|
||||
text: str
|
||||
choices: tuple[str, ...]
|
||||
answer: str
|
||||
confidence: float = 1.0
|
||||
|
||||
def parse_structured_text(content: str) -> list[ParsedItem]:
|
||||
"""Parse structured text using regex patterns."""
|
||||
pattern = re.compile(
|
||||
r"(?P<id>\d+)\.\s*(?P<text>.+?)\n"
|
||||
r"(?P<choices>(?:[A-D]\..+?\n)+)"
|
||||
r"Answer:\s*(?P<answer>[A-D])",
|
||||
re.MULTILINE | re.DOTALL,
|
||||
)
|
||||
items = []
|
||||
for match in pattern.finditer(content):
|
||||
choices = tuple(
|
||||
c.strip() for c in re.findall(r"[A-D]\.\s*(.+)", match.group("choices"))
|
||||
)
|
||||
items.append(ParsedItem(
|
||||
id=match.group("id"),
|
||||
text=match.group("text").strip(),
|
||||
choices=choices,
|
||||
answer=match.group("answer"),
|
||||
))
|
||||
return items
|
||||
```
|
||||
|
||||
### 2. 信頼度スコアリング
|
||||
|
||||
LLMによるレビューが必要かもしれない項目にフラグを立てる:
|
||||
|
||||
```python
|
||||
@dataclass(frozen=True)
|
||||
class ConfidenceFlag:
|
||||
item_id: str
|
||||
score: float
|
||||
reasons: tuple[str, ...]
|
||||
|
||||
def score_confidence(item: ParsedItem) -> ConfidenceFlag:
|
||||
"""Score extraction confidence and flag issues."""
|
||||
reasons = []
|
||||
score = 1.0
|
||||
|
||||
if len(item.choices) < 3:
|
||||
reasons.append("few_choices")
|
||||
score -= 0.3
|
||||
|
||||
if not item.answer:
|
||||
reasons.append("missing_answer")
|
||||
score -= 0.5
|
||||
|
||||
if len(item.text) < 10:
|
||||
reasons.append("short_text")
|
||||
score -= 0.2
|
||||
|
||||
return ConfidenceFlag(
|
||||
item_id=item.id,
|
||||
score=max(0.0, score),
|
||||
reasons=tuple(reasons),
|
||||
)
|
||||
|
||||
def identify_low_confidence(
|
||||
items: list[ParsedItem],
|
||||
threshold: float = 0.95,
|
||||
) -> list[ConfidenceFlag]:
|
||||
"""Return items below confidence threshold."""
|
||||
flags = [score_confidence(item) for item in items]
|
||||
return [f for f in flags if f.score < threshold]
|
||||
```
|
||||
|
||||
### 3. LLM バリデーター(エッジケースのみ)
|
||||
|
||||
```python
|
||||
def validate_with_llm(
|
||||
item: ParsedItem,
|
||||
original_text: str,
|
||||
client,
|
||||
) -> ParsedItem:
|
||||
"""Use LLM to fix low-confidence extractions."""
|
||||
response = client.messages.create(
|
||||
model="claude-haiku-4-5-20251001", # Cheapest model for validation
|
||||
max_tokens=500,
|
||||
messages=[{
|
||||
"role": "user",
|
||||
"content": (
|
||||
f"Extract the question, choices, and answer from this text.\n\n"
|
||||
f"Text: {original_text}\n\n"
|
||||
f"Current extraction: {item}\n\n"
|
||||
f"Return corrected JSON if needed, or 'CORRECT' if accurate."
|
||||
),
|
||||
}],
|
||||
)
|
||||
# Parse LLM response and return corrected item...
|
||||
return corrected_item
|
||||
```
|
||||
|
||||
### 4. ハイブリッドパイプライン
|
||||
|
||||
```python
|
||||
def process_document(
|
||||
content: str,
|
||||
*,
|
||||
llm_client=None,
|
||||
confidence_threshold: float = 0.95,
|
||||
) -> list[ParsedItem]:
|
||||
"""Full pipeline: regex -> confidence check -> LLM for edge cases."""
|
||||
# Step 1: Regex extraction (handles 95-98%)
|
||||
items = parse_structured_text(content)
|
||||
|
||||
# Step 2: Confidence scoring
|
||||
low_confidence = identify_low_confidence(items, confidence_threshold)
|
||||
|
||||
if not low_confidence or llm_client is None:
|
||||
return items
|
||||
|
||||
# Step 3: LLM validation (only for flagged items)
|
||||
low_conf_ids = {f.item_id for f in low_confidence}
|
||||
result = []
|
||||
for item in items:
|
||||
if item.id in low_conf_ids:
|
||||
result.append(validate_with_llm(item, content, llm_client))
|
||||
else:
|
||||
result.append(item)
|
||||
|
||||
return result
|
||||
```
|
||||
|
||||
## 実際のメトリクス
|
||||
|
||||
本番のクイズ解析パイプライン(410項目)より:
|
||||
|
||||
| メトリクス | 値 |
|
||||
|--------|-------|
|
||||
| 正規表現の成功率 | 98.0% |
|
||||
| 低信頼度項目 | 8 (2.0%) |
|
||||
| 必要なLLM呼び出し回数 | ~5 |
|
||||
| 全件LLM比のコスト節約 | ~95% |
|
||||
| テストカバレッジ | 93% |
|
||||
|
||||
## ベストプラクティス
|
||||
|
||||
* **正規表現から始める** — 不完全な正規表現でも改善のベースラインになる
|
||||
* **信頼度スコアリングを使用**して、LLMの助けが必要なものをプログラムで特定する
|
||||
* **最も安価なLLMを使用**して検証する(Haikuクラスのモデルで十分)
|
||||
* **解析済み項目を変更しない** — クリーニング/検証ステップから新しいインスタンスを返す
|
||||
* **TDDは解析器に効果的** — まず既知のパターンのテストを書き、次にエッジケースを書く
|
||||
* **メトリクスを記録**(正規表現の成功率、LLM呼び出し回数)してパイプラインの健全性を追跡する
|
||||
|
||||
## 避けるべきアンチパターン
|
||||
|
||||
* 正規表現が95%以上を処理できる場合に全テキストをLLMに送る(コスト高・低速)
|
||||
* 自由形式で高度に可変なテキストに正規表現を使用する(LLMの方が適切)
|
||||
* 信頼度スコアリングをスキップして正規表現が「うまくいく」ことを期待する
|
||||
* クリーニング/検証ステップで解析済みオブジェクトを変更する
|
||||
* エッジケースをテストしない(不正な入力、欠損フィールド、エンコーディング問題)
|
||||
|
||||
## 適用場面
|
||||
|
||||
* クイズ/試験問題の解析
|
||||
* フォームデータの抽出
|
||||
* 請求書/レシートの処理
|
||||
* ドキュメント構造の解析(見出し、セクション、表)
|
||||
* 繰り返しパターンがあり、コストが重要なあらゆる構造化テキスト
|
||||
43
docs/ja-JP/skills/remotion-video-creation/SKILL.md
Normal file
43
docs/ja-JP/skills/remotion-video-creation/SKILL.md
Normal file
@@ -0,0 +1,43 @@
|
||||
---
|
||||
name: remotion-video-creation
|
||||
description: Remotion のベストプラクティス - React で動画を作成する。3D、アニメーション、音声、字幕、チャート、トランジションなどをカバーするドメイン固有の29のルール。
|
||||
metadata:
|
||||
tags: remotion, video, react, animation, composition, three.js, lottie
|
||||
---
|
||||
|
||||
## 使用場面
|
||||
|
||||
Remotion のコードを扱い、ドメイン固有の知識が必要な場合にこのスキルを使用してください。
|
||||
|
||||
## 使い方
|
||||
|
||||
詳細な説明とコード例については、各ルールファイルをお読みください:
|
||||
|
||||
* [rules/3d.md](rules/3d.md) - Three.js と React Three Fiber を使用して Remotion で 3D コンテンツを作成する
|
||||
* [rules/animations.md](rules/animations.md) - Remotion の基本的なアニメーションスキル
|
||||
* [rules/assets.md](rules/assets.md) - Remotion で画像、動画、音声、フォントをインポートする
|
||||
* [rules/audio.md](rules/audio.md) - Remotion での音声とサウンドの使用——インポート、トリミング、音量、速度、ピッチ
|
||||
* [rules/calculate-metadata.md](rules/calculate-metadata.md) - コンポジションの長さ、サイズ、プロパティを動的に設定する
|
||||
* [rules/can-decode.md](rules/can-decode.md) - Mediabunny を使用してブラウザが動画をデコードできるか確認する
|
||||
* [rules/charts.md](rules/charts.md) - Remotion のチャートとデータビジュアライゼーションパターン
|
||||
* [rules/compositions.md](rules/compositions.md) - コンポジション、静止画、フォルダー、デフォルトプロパティ、動的メタデータの定義
|
||||
* [rules/display-captions.md](rules/display-captions.md) - TikTok スタイルのページと単語ハイライトに対応した Remotion での字幕表示
|
||||
* [rules/extract-frames.md](rules/extract-frames.md) - Mediabunny を使用して指定タイムスタンプの動画フレームを抽出する
|
||||
* [rules/fonts.md](rules/fonts.md) - Remotion で Google フォントとローカルフォントを読み込む
|
||||
* [rules/get-audio-duration.md](rules/get-audio-duration.md) - Mediabunny を使用して音声ファイルの長さ(秒)を取得する
|
||||
* [rules/get-video-dimensions.md](rules/get-video-dimensions.md) - Mediabunny を使用して動画ファイルの幅と高さを取得する
|
||||
* [rules/get-video-duration.md](rules/get-video-duration.md) - Mediabunny を使用して動画ファイルの長さ(秒)を取得する
|
||||
* [rules/gifs.md](rules/gifs.md) - Remotion のタイムラインと同期した GIF を表示する
|
||||
* [rules/images.md](rules/images.md) - Img コンポーネントを使用して Remotion に画像を埋め込む
|
||||
* [rules/import-srt-captions.md](rules/import-srt-captions.md) - @remotion/captions を使用して .srt 字幕ファイルを Remotion にインポートする
|
||||
* [rules/lottie.md](rules/lottie.md) - Remotion に Lottie アニメーションを埋め込む
|
||||
* [rules/measuring-dom-nodes.md](rules/measuring-dom-nodes.md) - Remotion で DOM 要素のサイズを測定する
|
||||
* [rules/measuring-text.md](rules/measuring-text.md) - テキストサイズの測定、コンテナへのテキスト適合、オーバーフローの確認
|
||||
* [rules/sequencing.md](rules/sequencing.md) - Remotion のシーケンスパターン——遅延、トリミング、項目の長さ制限
|
||||
* [rules/tailwind.md](rules/tailwind.md) - Remotion で TailwindCSS を使用する
|
||||
* [rules/text-animations.md](rules/text-animations.md) - Remotion のタイポグラフィとテキストアニメーションパターン
|
||||
* [rules/timing.md](rules/timing.md) - Remotion の補間曲線——線形、イージング、スプリングアニメーション
|
||||
* [rules/transcribe-captions.md](rules/transcribe-captions.md) - Remotion で字幕を生成するための音声文字起こし
|
||||
* [rules/transitions.md](rules/transitions.md) - Remotion のシーントランジションパターン
|
||||
* [rules/trimming.md](rules/trimming.md) - Remotion のトリミングパターン——アニメーションの最初または最後をトリミングする
|
||||
* [rules/videos.md](rules/videos.md) - Remotion への動画埋め込み——トリミング、音量、速度、ループ、ピッチ
|
||||
79
docs/ja-JP/skills/repo-scan/SKILL.md
Normal file
79
docs/ja-JP/skills/repo-scan/SKILL.md
Normal file
@@ -0,0 +1,79 @@
|
||||
---
|
||||
name: repo-scan
|
||||
description: クロススタックのソースコード資産監査——各ファイルを分類し、埋め込まれたサードパーティライブラリを検出し、各モジュールに対してインタラクティブなHTMLレポートとともに実用的な4段階の判定を提供する。
|
||||
origin: community
|
||||
---
|
||||
|
||||
# repo-scan
|
||||
|
||||
> どのエコシステムにも独自の依存関係マネージャーがあるが、C++、Android、iOS、Web をまたいで「どのコードが本当に自分のもので、どれがサードパーティで、どれが余分な負担か」を教えてくれるツールはない。
|
||||
|
||||
## 適用場面
|
||||
|
||||
* 大規模なレガシーコードベースを引き継ぎ、全体的な構造を把握する必要がある場合
|
||||
* 大規模なリファクタリング前——コアコード、重複コード、廃止コードを特定する
|
||||
* パッケージマネージャーで宣言せずにソースに直接埋め込まれたサードパーティの依存関係を監査する
|
||||
* モノレポの再編成に向けたアーキテクチャ決定記録を準備する
|
||||
|
||||
## インストール
|
||||
|
||||
```bash
|
||||
# Fetch only the pinned commit for reproducibility
|
||||
mkdir -p ~/.claude/skills/repo-scan
|
||||
git init repo-scan
|
||||
cd repo-scan
|
||||
git remote add origin https://github.com/haibindev/repo-scan.git
|
||||
git fetch --depth 1 origin 2742664
|
||||
git checkout --detach FETCH_HEAD
|
||||
cp -r . ~/.claude/skills/repo-scan
|
||||
```
|
||||
|
||||
> エージェントスキルをインストールする前に、ソースコードをレビューしてください。
|
||||
|
||||
## コア機能
|
||||
|
||||
| 機能 | 説明 |
|
||||
|---|---|
|
||||
| **クロススタックスキャン** | C/C++、Java/Android、iOS(OC/Swift)、Web(TS/JS/Vue)を一度にスキャン |
|
||||
| **ファイル分類** | 各ファイルをプロジェクトコード、サードパーティコード、またはビルドアーティファクトとしてマーク |
|
||||
| **ライブラリ検出** | 50以上の既知ライブラリ(FFmpeg、Boost、OpenSSL…)を識別しバージョン番号を抽出 |
|
||||
| **4段階の判定** | コア資産 / 抽出・統合 / 再構築 / 廃止 |
|
||||
| **HTMLレポート** | 階層的なドリルダウンナビゲーションに対応したインタラクティブなダークテーマページ |
|
||||
| **モノレポサポート** | 階層的スキャンによるサマリー + サブプロジェクトレポート |
|
||||
|
||||
## 分析の深さレベル
|
||||
|
||||
| レベル | 読み取りファイル数 | 適用場面 |
|
||||
|---|---|---|
|
||||
| `fast` | モジュールあたり1〜2個 | 大規模ディレクトリの素早い棚卸し |
|
||||
| `standard` | モジュールあたり2〜5個 | デフォルト監査、完全な依存関係 + アーキテクチャチェック |
|
||||
| `deep` | モジュールあたり5〜10個 | スレッド安全性、メモリ管理、API一貫性チェックを追加 |
|
||||
| `full` | 全ファイル | 統合前の包括的レビュー |
|
||||
|
||||
## 動作原理
|
||||
|
||||
1. **リポジトリの表面を分類**:ファイルを列挙し、各ファイルをプロジェクトコード、埋め込みサードパーティコード、ビルドアーティファクトとしてマークする。
|
||||
2. **埋め込みライブラリを検出**:ディレクトリ名、ヘッダーファイル、ライセンスファイル、バージョンマーカーを検査して、バンドルされた依存関係とその可能性のあるバージョンを識別する。
|
||||
3. **各モジュールをスコアリング**:ファイルをモジュールまたはサブシステムにグループ化し、所有権、重複度、保守コストに基づいて4つの判定のいずれかを割り当てる。
|
||||
4. **構造的リスクを強調**:冗長なアーティファクト、重複したラッパー、古いベンダーコード、および抽出・再構築・廃止すべきモジュールを指摘する。
|
||||
5. **レポートを生成**:簡潔なサマリーとインタラクティブなHTML出力を返し、モジュールごとのドリルダウンにより監査結果を非同期でレビューできる。
|
||||
|
||||
## 例
|
||||
|
||||
50,000ファイルのC++モノレポで:
|
||||
|
||||
* FFmpeg 2.x(2015年版)がまだ使用されていることを発見
|
||||
* 同じSDKラッパーが3回重複していることを発見
|
||||
* 636 MBのコミット済みDebug/ipch/objビルドアーティファクトを識別
|
||||
* 分類結果:3 MBのプロジェクトコード vs 596 MBのサードパーティコード
|
||||
|
||||
## ベストプラクティス
|
||||
|
||||
* 初回監査は `standard` の深さから始める
|
||||
* 100以上のモジュールを含むモノレポには `fast` で素早く棚卸しする
|
||||
* リファクタリングが必要とフラグ立てされたモジュールに対して段階的に `deep` を実行する
|
||||
* クロスモジュール分析の結果をレビューして、サブプロジェクト間の重複コードを検出する
|
||||
|
||||
## リンク
|
||||
|
||||
* [GitHub リポジトリ](https://github.com/haibindev/repo-scan)
|
||||
112
docs/ja-JP/skills/research-ops/SKILL.md
Normal file
112
docs/ja-JP/skills/research-ops/SKILL.md
Normal file
@@ -0,0 +1,112 @@
|
||||
---
|
||||
name: research-ops
|
||||
description: 証拠優先のECC現状調査ワークフロー。ユーザーが現在の公開証拠と提供されたローカルコンテキストに基づいて最新の事実、比較、情報の充実、または推奨事項を求める場合に使用する。
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# リサーチオペレーション
|
||||
|
||||
ユーザーが現在の情報の調査、オプションの比較、人物や企業情報の充実、または繰り返しのクエリをモニタリング可能なワークフローに変換することを求める場合に使用する。
|
||||
|
||||
これはリポジトリのリサーチスタックの操作ラッパーである。`deep-research`、`exa-search`、`market-research` の代替品ではなく、それらをいつどのように組み合わせて使うかを指示するものである。
|
||||
|
||||
## スキルスタック
|
||||
|
||||
関連する場面では、これらのECCネイティブスキルをワークフローに組み込む:
|
||||
|
||||
* `exa-search`:現在のウェブ情報の素早い発見に使用
|
||||
* `deep-research`:引用付きの複数ソース統合に使用
|
||||
* `market-research`:最終結果が推奨または優先順位付け決定である場合に使用
|
||||
* `lead-intelligence`:タスクが一般的な調査ではなく人物/企業を対象とする場合に使用
|
||||
* `knowledge-ops`:結果を後続のコンテキスト用に永続的に保存する必要がある場合に使用
|
||||
|
||||
## 使用場面
|
||||
|
||||
* ユーザーが「調査」「調べる」「比較」「誰に連絡すべきか」「最新情報」に言及する場合
|
||||
* 答えが現在の公開情報に依存する場合
|
||||
* ユーザーが証拠を提供し、それを新しい推奨事項に組み込んでほしい場合
|
||||
* タスクが繰り返し発生する可能性があり、一回限りのクエリではなくモニタリングに転換すべき場合
|
||||
|
||||
## 安全策
|
||||
|
||||
* 新鮮な検索が安価な場合、古い記憶に頼って現在の問いに答えない
|
||||
* 以下を区別する:
|
||||
* 出典付きの事実
|
||||
* ユーザーが提供した証拠
|
||||
* 推論
|
||||
* 推奨事項
|
||||
* 答えがローカルコードまたはドキュメントにすでに存在する場合、重い調査プロセスを起動しない
|
||||
|
||||
## ワークフロー
|
||||
|
||||
### 1. ユーザーがすでに提供した情報から始める
|
||||
|
||||
提供された素材を以下に正規化する:
|
||||
|
||||
* すでに証拠がある事実
|
||||
* 検証が必要な内容
|
||||
* 未解決の問い
|
||||
|
||||
ユーザーがすでに部分的なモデルを構築している場合、ゼロから再分析しない。
|
||||
|
||||
### 2. リクエストを分類する
|
||||
|
||||
検索前に正しいパスを選択する:
|
||||
|
||||
* 素早い事実的回答
|
||||
* 比較または意思決定メモ
|
||||
* リード/情報充実処理
|
||||
* 繰り返しモニタリング候補
|
||||
|
||||
### 3. 最も軽量な有効な証拠パスを優先する
|
||||
|
||||
* `exa-search` を使って素早い発見を行う
|
||||
* 統合または複数ソース情報が必要な場合、`deep-research` にアップグレードする
|
||||
* 結果を推奨形式で提示する必要がある場合、`market-research` を使用する
|
||||
* 実際のニーズがターゲット優先順位付けやウォームパス発見の場合、`lead-intelligence` に転送する
|
||||
|
||||
### 4. 証拠の境界を明示してレポートする
|
||||
|
||||
重要な主張については、それが以下のどれであるかを述べる:
|
||||
|
||||
* 出典付きの事実
|
||||
* ユーザーが提供したコンテキスト
|
||||
* 推論
|
||||
* 推奨事項
|
||||
|
||||
時効性の高い回答には具体的な日付を含める。
|
||||
|
||||
### 5. タスクを手動のままにすべきか決定する
|
||||
|
||||
ユーザーが同じ調査の問いを繰り返す可能性がある場合、それを明示し、永遠に同じ手動検索を繰り返すのではなく、モニタリングまたはワークフローレイヤーの採用を提案する。
|
||||
|
||||
## 出力フォーマット
|
||||
|
||||
```text
|
||||
問いのタイプ
|
||||
- 事実的 / 比較的 / 補完的 / モニタリング的
|
||||
|
||||
証拠
|
||||
- 出典付きの事実
|
||||
- ユーザーが提供したコンテキスト
|
||||
|
||||
推論
|
||||
- 証拠から導かれた結論
|
||||
|
||||
推奨事項
|
||||
- 答えまたは次のアクション
|
||||
- モニタリング項目にすべきか否か
|
||||
```
|
||||
|
||||
## よくある落とし穴
|
||||
|
||||
* 出典付きの事実と推論を、ラベルなしで混在させない
|
||||
* ユーザーが提供した証拠を無視しない
|
||||
* ローカルリポジトリのコンテキストで答えられる問いに重い調査パスを使わない
|
||||
* 日付のない時効性の高い答えを返さない
|
||||
|
||||
## 検証
|
||||
|
||||
* 重要な主張には証拠タイプのラベルを付ける
|
||||
* 時効性の高い出力には日付を含める
|
||||
* 最終的な推奨事項は実際に使用した調査モードと一致させる
|
||||
225
docs/ja-JP/skills/returns-reverse-logistics/SKILL.md
Normal file
225
docs/ja-JP/skills/returns-reverse-logistics/SKILL.md
Normal file
@@ -0,0 +1,225 @@
|
||||
---
|
||||
name: returns-reverse-logistics
|
||||
description: 返品承認、受取・検品、処分決定、返金処理、不正検出、保証クレーム管理のための標準化された専門知識。15年以上の経験を持つ返品オペレーションマネージャーの知見に基づく。段階的フレームワーク、処分経済性、不正パターン認識、ベンダー回収プロセスを含む。製品返品、逆物流、返金決定、返品不正検出、保証クレームを扱う場合に使用。license: Apache-2.0
|
||||
version: 1.0.0
|
||||
homepage: https://github.com/affaan-m/everything-claude-code
|
||||
origin: ECC
|
||||
metadata:
|
||||
author: evos
|
||||
clawdbot:
|
||||
emoji: ""
|
||||
---
|
||||
|
||||
# 返品と逆物流
|
||||
|
||||
## 役割と背景
|
||||
|
||||
あなたは15年以上の経験を持つシニア返品オペレーションマネージャーであり、小売・eコマース・オムニチャネル環境における完全な返品ライフサイクルを担当する。業務範囲は返品承認(RMA)、受取・検品、状態グレーディング、処分経路計画、返金・クレジット処理、不正検出、ベンダー回収(RTV)、保証クレーム管理に及ぶ。使用システムはOMS(注文管理システム)、WMS(倉庫管理システム)、RMS(返品管理システム)、CRM、不正検出プラットフォーム、ベンダーポータルである。顧客満足と利益保護、処理速度と検品精度、不正防止と正当な顧客へのフリクションのバランスを追求する。
|
||||
|
||||
## 使用場面
|
||||
|
||||
* 返品リクエストを処理し、RMAの適格性を判断する
|
||||
* 返品品を検品し、処分のための状態グレードを割り当てる
|
||||
* 処分決定経路(再販、リファービッシュ、クリアランス、廃棄、ベンダー返品)を計画する
|
||||
* 返品不正パターンや返品ポリシーの悪用を調査する
|
||||
* 保証クレームとベンダー回収の控除を管理する
|
||||
|
||||
## 運用方法
|
||||
|
||||
1. 返品リクエストを受け取り、返品ポリシー(時間ウィンドウ、状態、カテゴリー制限)に基づいて適格性を確認する
|
||||
2. 商品価値と返品理由に基づいて、プリペイドラベルまたはドロップポイント配送指示を含むRMAを発行する
|
||||
3. 返品センターで商品を受取・検品し、状態グレード(AからD)を割り当てる
|
||||
4. 回収経済性(再販利益 vs クリアランス vs 廃棄コスト)に基づいて最適な処分チャネルへ経路を設定する
|
||||
5. ポリシーに従って返金または交換を処理し、不正レビューのために異常をフラグ立てする
|
||||
6. ベンダーに請求可能な返品を集計し、契約上の規定ウィンドウ内にRTVクレームを提出する
|
||||
|
||||
## 例
|
||||
|
||||
* **高額電子機器返品**:顧客が「不具合あり」として1,200ドルのノートパソコンを返品。検品で外観の損傷が不具合の主張と矛盾していることが判明。グレーディング、リファービッシュコスト評価、処分経路計画(70%回収率でリファービッシュして再販 vs 85%回収率でベンダー返品)、不正フラグ評価を演習する。
|
||||
* **シリアル返品者検出**:顧客アカウントが6ヶ月間で23件の注文に対して47%の返品率を示している。不正指標に基づいてパターンを分析し、純利益貢献を計算し、ポリシーアクション(警告、返品制限、またはアカウントフラグ立て)を推奨する。
|
||||
* **保証クレーム紛争**:顧客が12ヶ月の保証期間の第11ヶ月に保証クレームを申告。製品が不適切な使用の兆候を示している。証拠書類を整理し、製造業者の保証除外基準を適用し、顧客へのコミュニケーション文書を起草する。
|
||||
|
||||
## コア知識
|
||||
|
||||
### 返品ポリシーロジック
|
||||
|
||||
すべての返品はポリシー評価から始まる。ポリシーエンジンは重複することがあり、時に矛盾するルールを考慮する必要がある:
|
||||
|
||||
* **標準返品ウィンドウ**:ほとんどの一般商品で受取後通常30日間。電子機器は通常15日間。生鮮品は返品不可。家具/マットレスは30〜90日間で特定の状態要件あり。延長された休日ウィンドウ(11月1日〜12月31日の購入は1月31日まで返品可能)は返品の急増を生み出し、1月中旬にピークを迎える。
|
||||
* **状態要件**:ほとんどのポリシーでは元の包装が完全で、すべての付属品が揃っており、合理的な検品を超えた使用の形跡がないことを要求する。「合理的な検品」は紛争の焦点——ノートパソコンの画面保護フィルムを剥がした顧客は技術的には製品を変更しているが、これは通常の開封行為である。
|
||||
* **レシートと購入証明**:クレジットカード、会員番号、電話番号によるPOSトランザクション検索が紙のレシートをほぼ置き換えている。ギフトレシートは現金返金ではなく購入価格での交換または店舗クレジットを付与する。レシートなし返品には上限がある(通常1件あたり50〜75ドル、12ヶ月間で3回)。最近の最低販売価格で返金される。
|
||||
* **再陳列手数料**:開封済みの電子機器(15%)、特注品(20〜25%)、大型/かさばる品物(返品運送の調整が必要)に適用される。欠陥製品や誤配送品は免除。顧客関係維持のための免除は利益への影響を意識する必要がある——28%の利益率の300ドル商品で45ドルの再陳列手数料を免除することは、見かけよりも実際のコストが高い。
|
||||
* **クロスチャネル返品**:オンライン購入・店舗返品(BORIS)は顧客が期待するが運用上複雑。オンライン価格と店舗価格が異なる場合がある。返金は現在の棚価格ではなく元の購入価格と一致させる。在庫システムは商品を店舗在庫に戻すか、配送センターへの返品としてフラグ立てできる必要がある。
|
||||
* **国際返品**:関税払い戻しの適格性は法定ウィンドウ内(国によって通常3〜5年)の再輸出証明を必要とする。低コスト商品では返品運送コストが商品価値を超えることがある——運賃が商品価値の40%を超える場合は「返品不要返金」を提供する。返品品の通関書類は元の輸出書類とは異なる。
|
||||
* **例外処理**:価格マッチ返品(より安い価格を見つけた顧客)、ウィンドウを超えた事情のある購入者の後悔、保証期間外の欠陥品、ロイヤルティレベルの適用(プレミアム顧客には延長ウィンドウと手数料免除)はすべて、硬直したルールではなく判断フレームワークを必要とする。
|
||||
|
||||
### 検品とグレーディング
|
||||
|
||||
返品品は処分決定を促す一貫したグレーディングを必要とする。速度と精度の間にはトレードオフがある——30秒の目視検査はスループットを処理できるが外観の欠陥を見落とす。5分の機能テストはすべての問題を見つけるがスケールのボトルネックを生む:
|
||||
|
||||
* **Aグレード(新品同様)**:元の包装が完全で、すべての付属品が揃い、使用の形跡なし、機能テスト合格。新品または「開封品」として再陳列でき、全額の利益回収が可能(小売価格の85〜100%)。目標検品時間:45〜90秒。
|
||||
* **Bグレード(良好)**:軽微な外観の摩耗、元の包装が損傷または外箱なし、すべての付属品揃い、機能的に正常。「開封品」または「リファービッシュ品」として小売価格の60〜80%で再陳列可能。再包装が必要な場合がある(1件あたり2〜5ドル)。目標検品時間:90〜180秒。
|
||||
* **Cグレード(普通)**:目に見える摩耗、傷、または軽微な損傷。単品価値の10%未満の付属品の欠如。機能的だが外観が損傷している。二次チャネル(アウトレット、マーケットプレイス、クリアランス)で小売価格の30〜50%で販売。リファービッシュコストが回収価値の20%未満なら修復可能。
|
||||
* **Dグレード(不良/部品取り用)**:機能不全、ひどい損傷、または主要部品の欠如。小売価格の5〜15%の価値で部品や材料として回収。部品回収が不可能なら、リサイクルまたは廃棄へ。
|
||||
|
||||
グレーディング基準はカテゴリーによって異なる。家電製品は機能テスト(電源オン、画面確認、接続性)が必要で、1件あたり2〜4分追加される。衣類の検品はシミ、臭い、生地の伸び、タグ欠如に焦点を当てる——経験豊富な検品員は「アームズレングス嗅覚テスト」とUVライトでシミを検出する。衛生規制により、化粧品や個人ケア用品は一度開封されると再陳列がほぼ不可能になる。
|
||||
|
||||
### 処分決定ツリー
|
||||
|
||||
処分は返品が価値を回収するか利益を損なうかが決まる環節。経路決定は経済性によって促される:
|
||||
|
||||
* **新品として再陳列**:包装完全なAグレード品のみ。必要な機能/安全テストを通過する必要がある。再ラベル貼りや再シールは規制上の問題を引き起こす可能性がある(「新品」と表示することに関するFTCの執行)。再陳列コスト(1件あたり3〜8ドル)が回収価値に対して小さい高利益商品に最適。
|
||||
* **再包装して「開封品」として販売**:包装が損傷したAグレード品またはBグレード品。再包装コスト(5〜15ドル、複雑さによる)は開封品価格と次の段階のチャネルの差益で正当化される必要がある。電子機器と家電は理想的。
|
||||
* **リファービッシュ**:リファービッシュコストがリファービッシュ後の販売価格の40%未満で、リファービッシュ販売チャネル(認定リファービッシュプログラム、メーカー直売店)が存在する場合に経済的に実行可能。高級電子機器、電動工具、家電で一般的。専用のリファービッシュステーション、スペアパーツ在庫、再テスト能力が必要。
|
||||
* **クリアランス**:Cグレードと一部のBグレード品で、再包装/リファービッシュが合理的でない場合。クリアランスチャネルにはパレットオークション(B-Stock、DirectLiquidation、Bulq)、卸売クリアランス業者(衣類は重量、電子機器は1件ごと)、地域クリアランス業者がある。回収率:小売価格の5〜20%。重要な洞察:パレット内のカテゴリーを混在させると価値が損なわれる——電子機器/衣類/家庭用品のパレットは最低カテゴリー価格で売れる。
|
||||
* **寄付**:公正市場価格(FMV)で税控除可能。FMVがクリアランス回収価値を上回り、会社が控除を活用するに十分な税負担がある場合、クリアランスよりも価値がある。ブランド保護:最終的に値引きチャネルに流れてブランドポジショニングを傷つける可能性のあるプライベートラベル品の寄付を制限する。
|
||||
* **廃棄**:リコール製品、返品フローで見つかった偽造品、規制上の処分要件がある製品(バッテリー、WEEE規制が必要な電子機器、危険物)、および二次市場での存在が受け入れられないブランド品に使用。コンプライアンスと税務書類のために廃棄証明が必要。
|
||||
|
||||
### 不正検出
|
||||
|
||||
返品不正は米国の小売業者に年間240億ドル以上の損失をもたらす。課題は正当な顧客への障壁を作らずに検出すること:
|
||||
|
||||
* **ワードローブ不正(着用後返品)**:顧客が衣類やアクセサリーを購入し、イベントに着用した後に返品する。指標:休日/イベント前後の返品集中、消臭剤の残留物、首元の化粧品汚れ、「試着」と矛盾した生地の折り目/伸び。対策:UVライトで化粧品汚れを確認、顧客に取り外しを指示していないRFIDタグを使用(タグがない場合は着用を示す)。
|
||||
* **レシート不正**:習得・盗取・偽造したレシートを使って盗品を返品し現金を得る。デジタルレシート検索が紙のレシートに取って代わるにつれ減少しているが、依然として発生。対策:すべての現金返金には身分証明書を要求、返品は元の支払い方法と一致させる、身分証明書ごとのレシートなし返品回数を制限する。
|
||||
* **すり替え不正(返品スイッチング)**:購入品の包装に偽造品、より安価な品物、または損傷した商品を入れて返品する。電子機器(新しい携帯電話の箱に古い携帯電話を入れて返品)や化粧品(容器をより安価な製品で再充填)でよく見られる。対策:返品時にシリアル番号を確認、重量が期待される製品の重量と一致するか確認、高額品は返金前に詳細検品を実施。
|
||||
* **シリアル返品者**:返品率が購入の30%超、または年間返品額が5,000ドル超の顧客。全員が不正というわけではない——本当に優柔不断だったり「ブラケット購入」(複数サイズを購入して試着)をしている人もいる。以下の次元で細分化する:返品理由の一貫性、返品時の製品状態、返品後の純生涯価値。年間5万ドル購入して1万8,000ドル返品(36%の返品率)しているが純収益3万2,000ドルの顧客は、年間1万5,000ドル購入で返品ゼロの顧客よりも価値がある。
|
||||
* **ブラケット購入**:複数のサイズ/色を意図的に注文し、大部分を返品する計画。合法的なショッピング行動だが、規模で見るとコストがかかる。フィッティングテクノロジー(サイズ推奨ツール、AR試着)、緩い交換ポリシー(無料交換、返品には再陳列手数料)、ペナルティではなく教育で対処する。
|
||||
* **価格裁定**:プロモーション/値引き期間に購入し、別の場所や時間に定価で返品して差益を得る。ポリシーは現在の価格に関わらず、実際の購入価格に返金を結びつける必要がある。クロスチャネル返品が主要な経路。
|
||||
* **組織的小売犯罪(ORC)**:複数の店舗/身元にわたって調整された盗難-返品操作。指標:同じ住所の複数の身分証明書による高額返品、頻繁に盗まれるカテゴリー(電子機器、化粧品、健康製品)の返品、地理的集中。損失防止(LP)チームに報告——これは標準的な返品オペレーションの範囲を超えている。
|
||||
|
||||
### ベンダー回収
|
||||
|
||||
すべての返品が顧客の責任というわけではない。欠陥品、フルフィルメントエラー、品質問題にはベンダーにコストを請求する経路がある:
|
||||
|
||||
* **ベンダー返品(RTV):** ベンダーの保証期間または欠陥クレームウィンドウ内に返品された欠陥品。プロセス:欠陥ユニットを蓄積する(RTVの最小出荷閾値はベンダーによって異なり、通常200〜500ドル)、RTV承認番号を取得する、ベンダー指定の返品施設に出荷する、クレジットの発行を追跡する。よくある失敗:RTV対象製品を返品倉庫内でベンダーのクレームウィンドウ(通常受取後90日)を超えて放置する。
|
||||
* **欠陥クレーム:** 欠陥率がベンダー契約の閾値(通常2〜5%)を超えた場合、超過分について正式な欠陥クレームを提出する。欠陥の書類(写真、検品記録、SKUごとに集計した顧客苦情データ)が必要。ベンダーは異議を唱える——データの品質が回収の成功率を決める。
|
||||
* **ベンダー控除:** ベンダーが引き起こした問題(ベンダーの配送センターからの誤出荷、製品のラベル誤り、包装の不具合)については、返品運送と処理人件費を含む全コストを控除する。ベンダーコンプライアンスプログラムと公表された基準と罰則が必要。
|
||||
* **クレジット vs 交換 vs 償却:** ベンダーが支払い能力があり迅速に対応できるなら、クレジットを争う。ベンダーが海外にいて回収が困難なら、交換を交渉する。クレームが小さく(200ドル未満)、ベンダーが主要なサプライヤーである場合は、償却を検討し、次の契約交渉に記録する。
|
||||
|
||||
### 保証管理
|
||||
|
||||
保証クレームは返品とは異なり、異なるワークフローに従う:
|
||||
|
||||
* **保証 vs 返品:** 返品は顧客が購入を取り消す権利を行使すること(通常30日以内、いかなる理由でも)。保証クレームは顧客が保証対象期間内(90日〜永続)に製品の欠陥を報告すること。異なるシステム、異なるポリシー、異なる財務処理。
|
||||
* **製造業者 vs 小売業者の責任:** 小売業者は通常、返品ウィンドウを担当する。製造業者は保証期間を担当する。グレーゾーン:保証期間内に繰り返し故障する「レモン」製品——顧客は返金を求め、製造業者は修理を提供し、小売業者は板挟みになる。
|
||||
* **延長保証/保護プラン:** 販売時点で販売され、30〜60%の利益率。延長保証に対するクレームは保証プロバイダー(通常はサードパーティ)が処理する。小売業者の役割はクレーム処理ではなくクレーム申告の支援。よくある苦情:顧客が小売業者の返品ポリシー、製造業者の保証、延長保証の補償を区別できない。
|
||||
|
||||
## 意思決定フレームワーク
|
||||
|
||||
### カテゴリーと状態による処分分類
|
||||
|
||||
| カテゴリー | Aグレード | Bグレード | Cグレード | Dグレード |
|
||||
|---|---|---|---|---|
|
||||
| 家電製品 | 再陳列(テスト後) | 開封品/リファービッシュ | ROI > 40%ならリファービッシュ、さもなければクリアランス | 部品回収または電子機器廃棄 |
|
||||
| 衣類 | タグが付いていれば再陳列 | 再包装/アウトレット | 重量別クリアランス | 繊維リサイクル |
|
||||
| 家庭用品・家具 | 再陳列 | 開封品値引き | クリアランス(ローカル、運送を避ける) | 寄付または廃棄 |
|
||||
| 健康・美容 | 密封されていれば再陳列 | 廃棄(規制上の理由) | 廃棄 | 廃棄 |
|
||||
| 書籍・メディア | 再陳列 | 再陳列(値引き) | クリアランス | リサイクル |
|
||||
| スポーツ用品 | 再陳列 | 開封品 | リファービッシュコスト < 価値の25%ならリファービッシュ | 部品回収または寄付 |
|
||||
| おもちゃ・ゲーム | 密封されていれば再陳列 | 開封品 | クリアランス | 安全基準を満たすなら寄付 |
|
||||
|
||||
### 不正スコアリングモデル
|
||||
|
||||
各返品を0〜100でスコアリング。65以上でレビューのフラグ立て、80以上で返金を保留:
|
||||
|
||||
| シグナル | スコア | 備考 |
|
||||
|---|---|---|
|
||||
| 返品率 > 30%(12ヶ月ローリング) | +15 | カテゴリー基準で調整 |
|
||||
| 受取後48時間以内の返品 | +5 | 正当な「比較購入」の可能性 |
|
||||
| 高額電子機器でシリアル番号不一致 | +40 | すり替え不正がほぼ確実 |
|
||||
| 申告時と受取時の返品理由が不一致 | +10 | 不一致フラグ |
|
||||
| 同週内の複数回返品 | +10 | 返品率シグナルと累積 |
|
||||
| 返品住所が出荷住所と異なる | +10 | ギフト返品を除く |
|
||||
| 製品重量が期待値と5%以上乖離 | +25 | すり替えまたは部品欠如 |
|
||||
| 顧客アカウント使用期間 < 30日 | +10 | 新規アカウントリスク |
|
||||
| レシートなし返品 | +15 | レシート不正リスクが高い |
|
||||
| 高損耗カテゴリーの商品 | +5 | 電子機器、化粧品、デザイナー衣類 |
|
||||
|
||||
### ベンダー回収のROI
|
||||
|
||||
以下の場合にベンダー回収を追求する:`(期待クレジット × 回収確率) > (人件費 + 運送コスト + 関係コスト)`。経験則:
|
||||
|
||||
* クレーム > 500ドル:必ず追求。50%の回収確率でも計算が成立する。
|
||||
* クレーム 200〜500ドル:ベンダーが実用的なRTVプログラムを持ちバルク出荷が可能なら追求。
|
||||
* クレーム < 200ドル:閾値に達するまで蓄積するか、次の発注に対して差し引く。個別ユニットは単独で出荷しない。
|
||||
* 海外ベンダー:最低閾値を1,000ドルに引き上げる。処理時間が30%増加することを見込む。
|
||||
|
||||
### 返品ポリシー例外処理ロジック
|
||||
|
||||
返品が標準ポリシーを超えた場合、以下の順序で評価する:
|
||||
|
||||
1. **製品に欠陥があるか?** ある場合、ウィンドウや状態に関わらず受け入れる。欠陥品は顧客ではなく会社の問題。
|
||||
2. **高価値の顧客か?**(顧客生涯価値で上位10%)そうであれば、受け入れて標準返金。顧客を維持する計算はほぼ常に例外を支持する。
|
||||
3. **リクエストは中立的な観察者にとって合理的か?** 顧客が11月に購入した冬物を3月に返品する(4ヶ月、30日ウィンドウを超える)のは理解できる。顧客が6月に購入した水着を12月に返品するのはあまり合理的ではない。
|
||||
4. **処分の結果は何か?** 製品が再陳列できる(Aグレード)なら、例外処理のコストは微々たるもの——承認。Cグレード以下なら、例外処理は実際の利益を損なう。
|
||||
5. **承認が先例リスクをもたらすか?** 記録された状況に対する一度限りの例外は、ほとんど先例を作らない。公開された例外処理(ソーシャルメディアの苦情)は常に先例を作る。
|
||||
|
||||
## 重要なエッジケース
|
||||
|
||||
これらは標準ワークフローでは処理できない状況である。必要に応じて特定のプロジェクトの運用マニュアルに展開できるよう、ここに簡単なサマリーを含める。
|
||||
|
||||
1. **ファームウェアが消去された高額電子機器:** 顧客が「欠陥あり」として返品したノートパソコンが、出荷時設定にリセットされており、6ヶ月分のバッテリーサイクル数を示している。デバイスは大量に使用されており、今は「欠陥品」として返品されている——グレーディングはクリーンなソフトウェア状態を超える必要がある。
|
||||
2. **不適切に梱包された危険物の返品:** 顧客がリチウム電池や化学品を含む製品を、必要なDOT包装なしで返品する。受け取ると規制上の責任が生じる。拒否すると顧客サービスの問題になる。製品は標準の小包返品輸送で返送できない。
|
||||
3. **関税が絡む国境を越えた返品:** 国際顧客が関税を支払って輸出した製品を返品する。関税払い戻し申請には顧客が持っていない特定の書類が必要。返品運送コストが製品価値を超える可能性がある。
|
||||
4. **コンテンツ制作後のインフルエンサーのバルク返品:** ソーシャルメディアのインフルエンサーが20点以上の商品を購入し、コンテンツを制作した後、1点を除いてすべて返品する。技術的にはポリシーに準拠しているが、ブランド価値はすでに抽出されている。開封動画が同じ商品を展示しているため、再陳列の課題が増大する。
|
||||
5. **顧客が改造した後の製品の保証クレーム:** 顧客が製品内のコンポーネントを交換し(例:ノートパソコンのRAMをアップグレード)、その後無関係な別のコンポーネント(例:画面の誤作動)に保証上の欠陥があると主張する。改造はクレームされた欠陥を保証の対象外にするかもしれないし、しないかもしれない。
|
||||
6. **高価値顧客かつ頻繁な返品者:** 年間8万ドル支出で42%の返品率の顧客。返品を禁止すると利益を生む顧客を失う。行動を受け入れると継続を奨励する。単純な返品率を超えた細かな顧客セグメンテーションが必要。
|
||||
7. **リコール製品の返品:** 顧客がアクティブな安全リコール対象の製品を返品する。標準の返品プロセスは誤り——リコール品はリコールプログラムに従うべきで、返品プログラムではない。混在させると責任とレポートのエラーが生じる。
|
||||
8. **現在の価格が購入価格より高いギフトレシートの返品:** ギフト受取者がギフトレシートを持って返品に来る。その商品は現在、贈り主が支払った価格より30ドル高く販売されている。ポリシーは購入価格での返金を定めているが、顧客は棚価格を見てその金額を期待している。
|
||||
|
||||
## コミュニケーションパターン
|
||||
|
||||
### トーンの調整
|
||||
|
||||
* **標準的な返金確認:** 温かく、効率的に。プロセスではなく、解決金額と期間を最初に述べる。
|
||||
* **返品拒否:** 共感的だが明確に。具体的なポリシーを説明し、代替案(交換、店舗クレジット、保証クレーム)を提供し、エスカレーションパスを提供する。顧客を選択肢なしにしない。
|
||||
* **不正調査による保留:** 中立的、事実に基づいて。「お客様の返品をさらに処理するのに時間が必要です」——顧客に「不正」や「調査」という言葉を絶対に使わない。タイムラインを提供する。不正指標の記録は内部コミュニケーションで行う。
|
||||
* **再陳列手数料の説明:** 透明に。手数料の対象(検品、再包装、価値の損失)を説明し、意外な結果を避けるために処理前に純返金額を確認する。
|
||||
* **ベンダーRTVクレーム:** 専門的、証拠に基づいて。欠陥データ、写真、SKUごとの返品量を含め、欠陥クレームをカバーするベンダー契約の条項を引用する。
|
||||
|
||||
### 重要テンプレート
|
||||
|
||||
以下に簡単なテンプレートを示す。不正、顧客体験、逆物流のワークフローに合わせて、本番使用前に調整すること。
|
||||
|
||||
**RMA承認:** 件名:`Return Approved — Order #{order_id}`。提供:RMA番号、返品配送指示、期待される返金タイムライン、状態要件。
|
||||
|
||||
**返金確認:** 金額を最初に述べる:「${amount}の返金を\[支払い方法]に処理しました。\[X]営業日をお待ちください。」
|
||||
|
||||
**不正保留通知:** 「お客様の返品は当社の処理チームによって審査されています。\[X]営業日以内に更新情報をお伝えする予定です。ご忍耐いただき、ありがとうございます。」
|
||||
|
||||
## エスカレーションプロトコル
|
||||
|
||||
### 自動エスカレーショントリガー
|
||||
|
||||
| トリガー | アクション | タイムライン |
|
||||
|---|---|---|
|
||||
| 返品価値 > 5,000ドル(単品) | 返金前にスーパーバイザーの承認が必要 | 処理前 |
|
||||
| 不正スコア ≥ 80 | 返金を保留し、不正レビューチームに転送 | 即時 |
|
||||
| 顧客がクレジットカードチャージバックを同時に申告 | 返品処理を停止し、支払いチームと調整 | 1時間以内 |
|
||||
| 製品がリコール品として識別 | リコールコーディネーターに転送し、標準返品として処理しない | 即時 |
|
||||
| SKUに対するベンダーの欠陥率が5%を超える | マーチャンダイジングとベンダー管理に通知 | 24時間以内 |
|
||||
| 同一顧客が12ヶ月以内に3回目のポリシー例外をリクエスト | 承認前にマネージャーのレビューが必要 | 処理前 |
|
||||
| 返品フローで疑わしい偽造品が発見 | 処理から取り出し、写真を撮り、損失防止とブランド保護に通知 | 即時 |
|
||||
| 返品に規制対象製品が含まれる(医薬品、危険物、医療機器) | コンプライアンスチームに転送 | 即時 |
|
||||
|
||||
### エスカレーションチェーン
|
||||
|
||||
レベル1(返品担当者) → レベル2(チームスーパーバイザー、2時間) → レベル3(返品マネージャー、8時間) → レベル4(オペレーションディレクター、24時間) → レベル5(副社長、48時間以上または単品返品 > 25,000ドル)
|
||||
|
||||
## パフォーマンス指標
|
||||
|
||||
| 指標 | 目標 | 危険フラグ |
|
||||
|---|---|---|
|
||||
| 返品処理時間(受取から返金まで) | < 48時間 | > 96時間 |
|
||||
| 検品精度(監査でのグレード一致性) | > 95% | < 88% |
|
||||
| 再陳列率(返品のうち新品/開封品として再陳列される割合) | > 45% | < 30% |
|
||||
| 不正検出率(確認された不正のうち捕捉された割合) | > 80% | < 60% |
|
||||
| 誤検知率(フラグが立てられた正当な返品の割合) | < 3% | > 8% |
|
||||
| ベンダー回収率(回収額 / 適格額) | > 70% | < 45% |
|
||||
| 顧客満足度(返品後CSAT) | > 4.2/5.0 | < 3.5/5.0 |
|
||||
| 返品処理1件あたりのコスト | < $8.00 | > $15.00 |
|
||||
|
||||
## その他のリソース
|
||||
|
||||
* このスキルを本番使用に移行する前に、グレーディング基準、不正レビュー閾値、返金承認マトリックスと組み合わせること。
|
||||
* 在庫補充基準、危険物返品処理、クリアランスルールは、実行決定を担う運用チームの近くに置くこと。
|
||||
264
docs/ja-JP/skills/rules-distill/SKILL.md
Normal file
264
docs/ja-JP/skills/rules-distill/SKILL.md
Normal file
@@ -0,0 +1,264 @@
|
||||
---
|
||||
name: rules-distill
|
||||
description: "スキルをスキャンしてドメイン横断的な原則を抽出し、ルールに蒸留する——既存のルールファイルへの追記、修正、または新規作成"
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# ルール蒸留
|
||||
|
||||
インストール済みのスキルをスキャンし、複数のスキルに現れる共通原則を抽出して、ルールとして蒸留する——既存のルールファイルに追記、時代遅れの内容を修正、または新しいルールファイルを作成する。
|
||||
|
||||
「決定論的収集 + LLM判断」原則を適用する:スクリプトが事実を網羅的に収集し、その後LLMが完全なコンテキストを通読して裁決を下す。
|
||||
|
||||
## 使用場面
|
||||
|
||||
* 定期的なルールメンテナンス(月次または新しいスキルのインストール後)
|
||||
* スキル棚卸し後、ルールにすべきパターンが見つかった場合
|
||||
* 使用中のスキルと比較してルールが不完全に感じられる場合
|
||||
|
||||
## 動作原理
|
||||
|
||||
ルール蒸留プロセスは3つのフェーズに従う:
|
||||
|
||||
### フェーズ 1:棚卸し(決定論的収集)
|
||||
|
||||
#### 1a. スキルインベントリの収集
|
||||
|
||||
```bash
|
||||
bash ~/.claude/skills/rules-distill/scripts/scan-skills.sh
|
||||
```
|
||||
|
||||
#### 1b. ルールインデックスの収集
|
||||
|
||||
```bash
|
||||
bash ~/.claude/skills/rules-distill/scripts/scan-rules.sh
|
||||
```
|
||||
|
||||
#### 1c. ユーザーへの提示
|
||||
|
||||
```
|
||||
ルール蒸留 — フェーズ 1:棚卸し
|
||||
────────────────────────────────────────
|
||||
スキル:{N} 個のファイルをスキャン
|
||||
ルール:{M} 個のファイルをインデックス化({K} 個の見出しを含む)
|
||||
|
||||
クロスリーディング分析を実行中...
|
||||
```
|
||||
|
||||
### フェーズ 2:通読、マッチング、裁決(LLM判断)
|
||||
|
||||
抽出とマッチングは単一の処理で統合的に行われる。ルールファイルは十分に小さく(合計約800行)、LLMに全文を提供できる——grepによる事前フィルタリングは不要。
|
||||
|
||||
#### バッチ処理
|
||||
|
||||
スキルの説明に基づいてスキルを**トピッククラスター**にグループ化する。各クラスターは、完全なルールテキストを提供されたサブエージェントで分析される。
|
||||
|
||||
#### バッチ間のマージ
|
||||
|
||||
全バッチ完了後、バッチ候補をマージする:
|
||||
|
||||
* 同一または重複する原則を持つ候補を重複排除する
|
||||
* 「2+スキル」要件を**全**バッチの統合証拠で再確認——各バッチでは1つのスキルにのみ現れるが、合計で2+スキルに現れる原則は有効
|
||||
|
||||
#### サブエージェントプロンプト
|
||||
|
||||
汎用エージェントを起動するために以下のプロンプトを使用する:
|
||||
|
||||
````
|
||||
あなたはスキルをクロスリーディングして、ルールに昇格すべき原則を抽出するアナリストです。
|
||||
|
||||
## 入力
|
||||
- スキル:{このバッチのスキルの全文}
|
||||
- 既存のルール:{全ルールファイルの全文}
|
||||
|
||||
## 抽出基準
|
||||
|
||||
**以下の条件を全て**満たす場合のみ候補原則を含める:
|
||||
|
||||
1. **2+スキルに現れる**:1つのスキルにのみ現れる原則はそのスキルに留める
|
||||
2. **実行可能な行動変容**:「Xをする」または「Yをしない」という形で書ける——「Xが重要」ではない
|
||||
3. **明確な違反リスク**:この原則を無視すると何が問題になるか(1文)
|
||||
4. **ルールにまだ存在しない**:全ルールテキストを確認——異なる言い回しで表現された概念も含めて
|
||||
|
||||
## マッチングと裁決
|
||||
|
||||
各候補原則を全ルールテキストと照合して裁決を下す:
|
||||
|
||||
- **追記**:既存のルールファイルの既存セクションに追加
|
||||
- **修正**:既存のルールの内容が不正確または不十分——修正案を提案
|
||||
- **新セクション**:既存のルールファイルに新しいセクションを追加
|
||||
- **新ファイル**:新しいルールファイルを作成
|
||||
- **対応済み**:既存のルールで十分にカバー済み(言い回しが異なっても)
|
||||
- **過度に具体的**:スキルレベルに留めるべき
|
||||
|
||||
## 出力フォーマット(各候補原則)
|
||||
|
||||
```json
|
||||
{
|
||||
"principle": "1〜2文、'Xをする' / 'Yをしない' の形式",
|
||||
"evidence": ["スキル名: §セクション", "スキル名: §セクション"],
|
||||
"violation_risk": "1文",
|
||||
"verdict": "追記 / 修正 / 新セクション / 新ファイル / 対応済み / 過度に具体的",
|
||||
"target_rule": "ファイル名 §セクション、または '新規'",
|
||||
"confidence": "高 / 中 / 低",
|
||||
"draft": "'追記'/'新セクション'/'新ファイル' 裁決のための草案テキスト",
|
||||
"revision": {
|
||||
"reason": "既存の内容が不正確または不十分な理由('修正' 裁決のみ)",
|
||||
"before": "置き換える現在のテキスト('修正' 裁決のみ)",
|
||||
"after": "提案する置き換えテキスト('修正' 裁決のみ)"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 除外事項
|
||||
|
||||
- ルールにすでに存在する明らかな原則
|
||||
- 言語/フレームワーク固有の知識(言語固有のルールまたはスキルに属する)
|
||||
- コード例とコマンド(スキルに属する)
|
||||
````
|
||||
|
||||
#### 裁決リファレンス
|
||||
|
||||
| 裁決 | 意味 | ユーザーへの提示 |
|
||||
|---------|---------|-------------------|
|
||||
| **追記** | 既存セクションに追加 | ターゲット + 草案 |
|
||||
| **修正** | 不正確/不十分な内容を修正 | ターゲット + 理由 + 修正前/後 |
|
||||
| **新セクション** | 既存ファイルに新セクションを追加 | ターゲット + 草案 |
|
||||
| **新ファイル** | 新しいルールファイルを作成 | ファイル名 + 完全な草案 |
|
||||
| **対応済み** | ルールでカバー済み(言い回しが異なる場合も) | 理由(1行) |
|
||||
| **過度に具体的** | スキルに留めるべき | 関連スキルへのリンク |
|
||||
|
||||
#### 裁決の品質要件
|
||||
|
||||
```
|
||||
# 良い例
|
||||
rules/common/security.md の §入力検証 セクションに追加:
|
||||
「メモリや知識ベースに保存されたLLM出力は信頼できないデータとして扱う——書き込み時にサニタイズし、読み取り時に検証する。」
|
||||
根拠:llm-memory-trust-boundary と llm-social-agent-anti-pattern の両方が累積的なプロンプトインジェクションリスクを説明している。現在の security.md は人間による入力検証のみをカバーしており、LLM出力の信頼境界の説明が欠けている。
|
||||
|
||||
# 悪い例
|
||||
security.md に追記:LLMセキュリティ原則を追加
|
||||
```
|
||||
|
||||
### フェーズ 3:ユーザーレビューと実行
|
||||
|
||||
#### サマリーテーブル
|
||||
|
||||
```
|
||||
# ルール蒸留レポート
|
||||
|
||||
## 概要
|
||||
スキャンしたスキル数:{N} | ルールファイル数:{M} | 候補ルール数:{K}
|
||||
|
||||
| # | 原則 | 判定 | ターゲットファイル/セクション | 信頼度 |
|
||||
|---|-----------|---------|--------|------------|
|
||||
| 1 | ... | 追記 | security.md §入力検証 | 高 |
|
||||
| 2 | ... | 修正 | testing.md §テスト駆動開発 | 中 |
|
||||
| 3 | ... | 新セクション | coding-style.md | 高 |
|
||||
| 4 | ... | 過度に具体的 | — | — |
|
||||
|
||||
## 詳細
|
||||
(各候補ルールの詳細:証拠、違反リスク、草案テキスト)
|
||||
```
|
||||
|
||||
#### ユーザーアクション
|
||||
|
||||
ユーザーは番号で以下を応答する:
|
||||
|
||||
* **承認**:草案をそのままルールに適用する
|
||||
* **修正**:適用前に草案を編集する
|
||||
* **スキップ**:この候補ルールを適用しない
|
||||
|
||||
**ルールを自動的に変更しない。常にユーザーの承認が必要。**
|
||||
|
||||
#### 結果の保存
|
||||
|
||||
結果をスキルディレクトリ(`results.json`)に保存する:
|
||||
|
||||
* **タイムスタンプ形式**:`date -u +%Y-%m-%dT%H:%M:%SZ`(UTC、秒精度)
|
||||
* **候補ID形式**:原則に基づいたケバブケース(例:`llm-output-trust-boundary`)
|
||||
|
||||
```json
|
||||
{
|
||||
"distilled_at": "2026-03-18T10:30:42Z",
|
||||
"skills_scanned": 56,
|
||||
"rules_scanned": 22,
|
||||
"candidates": {
|
||||
"llm-output-trust-boundary": {
|
||||
"principle": "Treat LLM output as untrusted when stored or re-injected",
|
||||
"verdict": "Append",
|
||||
"target": "rules/common/security.md",
|
||||
"evidence": ["llm-memory-trust-boundary", "llm-social-agent-anti-pattern"],
|
||||
"status": "applied"
|
||||
},
|
||||
"iteration-bounds": {
|
||||
"principle": "Define explicit stop conditions for all iteration loops",
|
||||
"verdict": "New Section",
|
||||
"target": "rules/common/coding-style.md",
|
||||
"evidence": ["iterative-retrieval", "continuous-agent-loop", "agent-harness-construction"],
|
||||
"status": "skipped"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 例
|
||||
|
||||
### エンドツーエンド実行
|
||||
|
||||
```
|
||||
$ /rules-distill
|
||||
|
||||
ルール蒸留 — フェーズ 1:棚卸し
|
||||
────────────────────────────────────────
|
||||
スキル:56個のファイルをスキャン済み
|
||||
ルール:22個のファイル(75個の見出しをインデックス化)
|
||||
|
||||
クロスリーディング分析を実行中...
|
||||
|
||||
[サブエージェント分析:バッチ 1 (agent/meta スキル) ...]
|
||||
[サブエージェント分析:バッチ 2 (coding/pattern スキル) ...]
|
||||
[バッチ間マージ:2件の重複を削除、1件のクロスバッチ候補が昇格]
|
||||
|
||||
# ルール蒸留レポート
|
||||
|
||||
## サマリー
|
||||
スキャン済みスキル:56 | ルール:22個のファイル | 候補:4
|
||||
|
||||
| # | 原則 | 判定 | ターゲット | 信頼度 |
|
||||
|---|-----------|---------|--------|------------|
|
||||
| 1 | LLM出力:再利用前に正規化、型チェック、サニタイズ | 新セクション | coding-style.md | 高 |
|
||||
| 2 | 反復ループに明示的な停止条件を定義する | 新セクション | coding-style.md | 高 |
|
||||
| 3 | タスクの途中ではなくフェーズ境界でコンテキストを圧縮する | 追記 | performance.md §コンテキストウィンドウ | 高 |
|
||||
| 4 | ビジネスロジックをI/Oフレームワーク型から分離する | 新セクション | patterns.md | 高 |
|
||||
|
||||
## 詳細
|
||||
|
||||
### 1. LLM出力検証
|
||||
判定:coding-style.md に新セクションを作成
|
||||
証拠:parallel-subagent-batch-merge, llm-social-agent-anti-pattern, llm-memory-trust-boundary
|
||||
違反リスク:LLM出力の形式ドリフト、型不一致、構文エラーにより下流処理がクラッシュする
|
||||
草案:
|
||||
## LLM出力検証
|
||||
LLM出力を再利用する前に、正規化、型チェック、サニタイズを行う...
|
||||
参照スキル:parallel-subagent-batch-merge, llm-memory-trust-boundary
|
||||
|
||||
[... 候補 2〜4 の詳細 ...]
|
||||
|
||||
各候補を番号で承認、修正、スキップ:
|
||||
> ユーザー:1, 3 を承認。2, 4 をスキップ。
|
||||
|
||||
✓ 適用済み:coding-style.md §LLM出力検証
|
||||
✓ 適用済み:performance.md §コンテキストウィンドウ管理
|
||||
✗ スキップ:反復境界
|
||||
✗ スキップ:境界型変換
|
||||
|
||||
results.json に結果を保存済み
|
||||
```
|
||||
|
||||
## 設計原則
|
||||
|
||||
* **何をするかではなく、何か**:原則のみを抽出する(ルールの範囲)。コード例とコマンドはスキルに留める。
|
||||
* **ソースへのリンクを維持**:草案テキストには `参照スキル:[名前]` への参照を含め、読者が詳細な「どうするか」を見つけられるようにする。
|
||||
* **決定論的収集、LLM判断**:スクリプトが網羅性を保証し、LLMがコンテキスト理解を保証する。
|
||||
* **抽象化防止策**:3層フィルター(2+スキルの証拠、実行可能行動テスト、違反リスク)により、過度に抽象的な原則がルールに入ることを防ぐ。
|
||||
499
docs/ja-JP/skills/rust-patterns/SKILL.md
Normal file
499
docs/ja-JP/skills/rust-patterns/SKILL.md
Normal file
@@ -0,0 +1,499 @@
|
||||
---
|
||||
name: rust-patterns
|
||||
description: 慣用的なRustパターン、所有権、エラー処理、トレイト、並行処理、および安全で高性能なアプリケーションを構築するためのベストプラクティス。
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Rust 開発パターン
|
||||
|
||||
安全で高性能かつ保守性の高いアプリケーションを構築するための慣用的なRustパターンとベストプラクティス。
|
||||
|
||||
## 使用場面
|
||||
|
||||
* 新しいRustコードを書く場合
|
||||
* Rustコードをレビューする場合
|
||||
* 既存のRustコードをリファクタリングする場合
|
||||
* クレート構造とモジュールレイアウトを設計する場合
|
||||
|
||||
## 動作原理
|
||||
|
||||
このスキルは6つの重要な領域で慣用的なRustの規約を強制する:コンパイル時のデータ競合防止のための所有権と借用、ライブラリでは`thiserror`、アプリケーションでは`anyhow`を使用した`Result`/`?`エラー伝播、不正な状態を表現不可能にする列挙型と完全パターンマッチング、ゼロコスト抽象化のためのトレイトとジェネリクス、`Arc<Mutex<T>>`、チャンネル、async/awaitによる安全な並行処理、ドメインで整理された最小化された`pub`インターフェース。
|
||||
|
||||
## コア原則
|
||||
|
||||
### 1. 所有権と借用
|
||||
|
||||
Rustの所有権システムはコンパイル時にデータ競合とメモリエラーを防ぐ。
|
||||
|
||||
```rust
|
||||
// Good: Pass references when you don't need ownership
|
||||
fn process(data: &[u8]) -> usize {
|
||||
data.len()
|
||||
}
|
||||
|
||||
// Good: Take ownership only when you need to store or consume
|
||||
fn store(data: Vec<u8>) -> Record {
|
||||
Record { payload: data }
|
||||
}
|
||||
|
||||
// Bad: Cloning unnecessarily to avoid borrow checker
|
||||
fn process_bad(data: &Vec<u8>) -> usize {
|
||||
let cloned = data.clone(); // Wasteful — just borrow
|
||||
cloned.len()
|
||||
}
|
||||
```
|
||||
|
||||
### 柔軟な所有権のための `Cow` の使用
|
||||
|
||||
```rust
|
||||
use std::borrow::Cow;
|
||||
|
||||
fn normalize(input: &str) -> Cow<'_, str> {
|
||||
if input.contains(' ') {
|
||||
Cow::Owned(input.replace(' ', "_"))
|
||||
} else {
|
||||
Cow::Borrowed(input) // Zero-cost when no mutation needed
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## エラー処理
|
||||
|
||||
### `Result` と `?` を使用する——本番環境では `unwrap()` を絶対に使わない
|
||||
|
||||
```rust
|
||||
// Good: Propagate errors with context
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
fn load_config(path: &str) -> Result<Config> {
|
||||
let content = std::fs::read_to_string(path)
|
||||
.with_context(|| format!("failed to read config from {path}"))?;
|
||||
let config: Config = toml::from_str(&content)
|
||||
.with_context(|| format!("failed to parse config from {path}"))?;
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
// Bad: Panics on error
|
||||
fn load_config_bad(path: &str) -> Config {
|
||||
let content = std::fs::read_to_string(path).unwrap(); // Panics!
|
||||
toml::from_str(&content).unwrap()
|
||||
}
|
||||
```
|
||||
|
||||
### ライブラリエラーには `thiserror`、アプリケーションエラーには `anyhow`
|
||||
|
||||
```rust
|
||||
// Library code: structured, typed errors
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum StorageError {
|
||||
#[error("record not found: {id}")]
|
||||
NotFound { id: String },
|
||||
#[error("connection failed")]
|
||||
Connection(#[from] std::io::Error),
|
||||
#[error("invalid data: {0}")]
|
||||
InvalidData(String),
|
||||
}
|
||||
|
||||
// Application code: flexible error handling
|
||||
use anyhow::{bail, Result};
|
||||
|
||||
fn run() -> Result<()> {
|
||||
let config = load_config("app.toml")?;
|
||||
if config.workers == 0 {
|
||||
bail!("worker count must be > 0");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### ネストしたマッチの代わりに `Option` コンビネーターを優先する
|
||||
|
||||
```rust
|
||||
// Good: Combinator chain
|
||||
fn find_user_email(users: &[User], id: u64) -> Option<String> {
|
||||
users.iter()
|
||||
.find(|u| u.id == id)
|
||||
.map(|u| u.email.clone())
|
||||
}
|
||||
|
||||
// Bad: Deeply nested matching
|
||||
fn find_user_email_bad(users: &[User], id: u64) -> Option<String> {
|
||||
match users.iter().find(|u| u.id == id) {
|
||||
Some(user) => match &user.email {
|
||||
email => Some(email.clone()),
|
||||
},
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 列挙型とパターンマッチング
|
||||
|
||||
### 状態を列挙型としてモデル化する
|
||||
|
||||
```rust
|
||||
// Good: Impossible states are unrepresentable
|
||||
enum ConnectionState {
|
||||
Disconnected,
|
||||
Connecting { attempt: u32 },
|
||||
Connected { session_id: String },
|
||||
Failed { reason: String, retries: u32 },
|
||||
}
|
||||
|
||||
fn handle(state: &ConnectionState) {
|
||||
match state {
|
||||
ConnectionState::Disconnected => connect(),
|
||||
ConnectionState::Connecting { attempt } if *attempt > 3 => abort(),
|
||||
ConnectionState::Connecting { .. } => wait(),
|
||||
ConnectionState::Connected { session_id } => use_session(session_id),
|
||||
ConnectionState::Failed { retries, .. } if *retries < 5 => retry(),
|
||||
ConnectionState::Failed { reason, .. } => log_failure(reason),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 完全マッチング——ビジネスロジックではワイルドカードを使わない
|
||||
|
||||
```rust
|
||||
// Good: Handle every variant explicitly
|
||||
match command {
|
||||
Command::Start => start_service(),
|
||||
Command::Stop => stop_service(),
|
||||
Command::Restart => restart_service(),
|
||||
// Adding a new variant forces handling here
|
||||
}
|
||||
|
||||
// Bad: Wildcard hides new variants
|
||||
match command {
|
||||
Command::Start => start_service(),
|
||||
_ => {} // Silently ignores Stop, Restart, and future variants
|
||||
}
|
||||
```
|
||||
|
||||
## トレイトとジェネリクス
|
||||
|
||||
### ジェネリックを受け取り、具体的な型を返す
|
||||
|
||||
```rust
|
||||
// Good: Generic input, concrete output
|
||||
fn read_all(reader: &mut impl Read) -> std::io::Result<Vec<u8>> {
|
||||
let mut buf = Vec::new();
|
||||
reader.read_to_end(&mut buf)?;
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
// Good: Trait bounds for multiple constraints
|
||||
fn process<T: Display + Send + 'static>(item: T) -> String {
|
||||
format!("processed: {item}")
|
||||
}
|
||||
```
|
||||
|
||||
### 動的ディスパッチにトレイトオブジェクトを使用する
|
||||
|
||||
```rust
|
||||
// Use when you need heterogeneous collections or plugin systems
|
||||
trait Handler: Send + Sync {
|
||||
fn handle(&self, request: &Request) -> Response;
|
||||
}
|
||||
|
||||
struct Router {
|
||||
handlers: Vec<Box<dyn Handler>>,
|
||||
}
|
||||
|
||||
// Use generics when you need performance (monomorphization)
|
||||
fn fast_process<H: Handler>(handler: &H, request: &Request) -> Response {
|
||||
handler.handle(request)
|
||||
}
|
||||
```
|
||||
|
||||
### 型安全のためにNewTypeパターンを使用する
|
||||
|
||||
```rust
|
||||
// Good: Distinct types prevent mixing up arguments
|
||||
struct UserId(u64);
|
||||
struct OrderId(u64);
|
||||
|
||||
fn get_order(user: UserId, order: OrderId) -> Result<Order> {
|
||||
// Can't accidentally swap user and order IDs
|
||||
todo!()
|
||||
}
|
||||
|
||||
// Bad: Easy to swap arguments
|
||||
fn get_order_bad(user_id: u64, order_id: u64) -> Result<Order> {
|
||||
todo!()
|
||||
}
|
||||
```
|
||||
|
||||
## 構造体とデータモデリング
|
||||
|
||||
### 複雑な構築にはビルダーパターンを使用する
|
||||
|
||||
```rust
|
||||
struct ServerConfig {
|
||||
host: String,
|
||||
port: u16,
|
||||
max_connections: usize,
|
||||
}
|
||||
|
||||
impl ServerConfig {
|
||||
fn builder(host: impl Into<String>, port: u16) -> ServerConfigBuilder {
|
||||
ServerConfigBuilder { host: host.into(), port, max_connections: 100 }
|
||||
}
|
||||
}
|
||||
|
||||
struct ServerConfigBuilder { host: String, port: u16, max_connections: usize }
|
||||
|
||||
impl ServerConfigBuilder {
|
||||
fn max_connections(mut self, n: usize) -> Self { self.max_connections = n; self }
|
||||
fn build(self) -> ServerConfig {
|
||||
ServerConfig { host: self.host, port: self.port, max_connections: self.max_connections }
|
||||
}
|
||||
}
|
||||
|
||||
// Usage: ServerConfig::builder("localhost", 8080).max_connections(200).build()
|
||||
```
|
||||
|
||||
## イテレーターとクロージャー
|
||||
|
||||
### 手動ループの代わりにイテレーターチェーンを優先する
|
||||
|
||||
```rust
|
||||
// Good: Declarative, lazy, composable
|
||||
let active_emails: Vec<String> = users.iter()
|
||||
.filter(|u| u.is_active)
|
||||
.map(|u| u.email.clone())
|
||||
.collect();
|
||||
|
||||
// Bad: Imperative accumulation
|
||||
let mut active_emails = Vec::new();
|
||||
for user in &users {
|
||||
if user.is_active {
|
||||
active_emails.push(user.email.clone());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 型注釈付きの `collect()` を使用する
|
||||
|
||||
```rust
|
||||
// Collect into different types
|
||||
let names: Vec<_> = items.iter().map(|i| &i.name).collect();
|
||||
let lookup: HashMap<_, _> = items.iter().map(|i| (i.id, i)).collect();
|
||||
let combined: String = parts.iter().copied().collect();
|
||||
|
||||
// Collect Results — short-circuits on first error
|
||||
let parsed: Result<Vec<i32>, _> = strings.iter().map(|s| s.parse()).collect();
|
||||
```
|
||||
|
||||
## 並行処理
|
||||
|
||||
### 共有可変状態には `Arc<Mutex<T>>` を使用する
|
||||
|
||||
```rust
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
let counter = Arc::new(Mutex::new(0));
|
||||
let handles: Vec<_> = (0..10).map(|_| {
|
||||
let counter = Arc::clone(&counter);
|
||||
std::thread::spawn(move || {
|
||||
let mut num = counter.lock().expect("mutex poisoned");
|
||||
*num += 1;
|
||||
})
|
||||
}).collect();
|
||||
|
||||
for handle in handles {
|
||||
handle.join().expect("worker thread panicked");
|
||||
}
|
||||
```
|
||||
|
||||
### メッセージパッシングにチャンネルを使用する
|
||||
|
||||
```rust
|
||||
use std::sync::mpsc;
|
||||
|
||||
let (tx, rx) = mpsc::sync_channel(16); // Bounded channel with backpressure
|
||||
|
||||
for i in 0..5 {
|
||||
let tx = tx.clone();
|
||||
std::thread::spawn(move || {
|
||||
tx.send(format!("message {i}")).expect("receiver disconnected");
|
||||
});
|
||||
}
|
||||
drop(tx); // Close sender so rx iterator terminates
|
||||
|
||||
for msg in rx {
|
||||
println!("{msg}");
|
||||
}
|
||||
```
|
||||
|
||||
### Tokioを使用した非同期プログラミング
|
||||
|
||||
```rust
|
||||
use tokio::time::Duration;
|
||||
|
||||
async fn fetch_with_timeout(url: &str) -> Result<String> {
|
||||
let response = tokio::time::timeout(
|
||||
Duration::from_secs(5),
|
||||
reqwest::get(url),
|
||||
)
|
||||
.await
|
||||
.context("request timed out")?
|
||||
.context("request failed")?;
|
||||
|
||||
response.text().await.context("failed to read body")
|
||||
}
|
||||
|
||||
// Spawn concurrent tasks
|
||||
async fn fetch_all(urls: Vec<String>) -> Vec<Result<String>> {
|
||||
let handles: Vec<_> = urls.into_iter()
|
||||
.map(|url| tokio::spawn(async move {
|
||||
fetch_with_timeout(&url).await
|
||||
}))
|
||||
.collect();
|
||||
|
||||
let mut results = Vec::with_capacity(handles.len());
|
||||
for handle in handles {
|
||||
results.push(handle.await.unwrap_or_else(|e| panic!("spawned task panicked: {e}")));
|
||||
}
|
||||
results
|
||||
}
|
||||
```
|
||||
|
||||
## アンセーフコード
|
||||
|
||||
### Unsafe を使用できる場合
|
||||
|
||||
```rust
|
||||
// Acceptable: FFI boundary with documented invariants (Rust 2024+)
|
||||
/// # Safety
|
||||
/// `ptr` must be a valid, aligned pointer to an initialized `Widget`.
|
||||
unsafe fn widget_from_raw<'a>(ptr: *const Widget) -> &'a Widget {
|
||||
// SAFETY: caller guarantees ptr is valid and aligned
|
||||
unsafe { &*ptr }
|
||||
}
|
||||
|
||||
// Acceptable: Performance-critical path with proof of correctness
|
||||
// SAFETY: index is always < len due to the loop bound
|
||||
unsafe { slice.get_unchecked(index) }
|
||||
```
|
||||
|
||||
### Unsafe を使用してはいけない場合
|
||||
|
||||
```rust
|
||||
// Bad: Using unsafe to bypass borrow checker
|
||||
// Bad: Using unsafe for convenience
|
||||
// Bad: Using unsafe without a Safety comment
|
||||
// Bad: Transmuting between unrelated types
|
||||
```
|
||||
|
||||
## モジュールシステムとクレート構造
|
||||
|
||||
### 型ではなくドメインで整理する
|
||||
|
||||
```text
|
||||
my_app/
|
||||
├── src/
|
||||
│ ├── main.rs
|
||||
│ ├── lib.rs
|
||||
│ ├── auth/ # ドメインモジュール
|
||||
│ │ ├── mod.rs
|
||||
│ │ ├── token.rs
|
||||
│ │ └── middleware.rs
|
||||
│ ├── orders/ # ドメインモジュール
|
||||
│ │ ├── mod.rs
|
||||
│ │ ├── model.rs
|
||||
│ │ └── service.rs
|
||||
│ └── db/ # インフラストラクチャ
|
||||
│ ├── mod.rs
|
||||
│ └── pool.rs
|
||||
├── tests/ # 統合テスト
|
||||
├── benches/ # ベンチマーク
|
||||
└── Cargo.toml
|
||||
```
|
||||
|
||||
### 可視性——露出を最小化する
|
||||
|
||||
```rust
|
||||
// Good: pub(crate) for internal sharing
|
||||
pub(crate) fn validate_input(input: &str) -> bool {
|
||||
!input.is_empty()
|
||||
}
|
||||
|
||||
// Good: Re-export public API from lib.rs
|
||||
pub mod auth;
|
||||
pub use auth::AuthMiddleware;
|
||||
|
||||
// Bad: Making everything pub
|
||||
pub fn internal_helper() {} // Should be pub(crate) or private
|
||||
```
|
||||
|
||||
## ツール統合
|
||||
|
||||
### 基本コマンド
|
||||
|
||||
```bash
|
||||
# Build and check
|
||||
cargo build
|
||||
cargo check # Fast type checking without codegen
|
||||
cargo clippy # Lints and suggestions
|
||||
cargo fmt # Format code
|
||||
|
||||
# Testing
|
||||
cargo test
|
||||
cargo test -- --nocapture # Show println output
|
||||
cargo test --lib # Unit tests only
|
||||
cargo test --test integration # Integration tests only
|
||||
|
||||
# Dependencies
|
||||
cargo audit # Security audit
|
||||
cargo tree # Dependency tree
|
||||
cargo update # Update dependencies
|
||||
|
||||
# Performance
|
||||
cargo bench # Run benchmarks
|
||||
```
|
||||
|
||||
## クイックリファレンス:Rustのイディオム
|
||||
|
||||
| イディオム | 説明 |
|
||||
|-------|-------------|
|
||||
| 借用、クローンではなく | 所有権が必要でなければ `&T` を渡し、クローンしない |
|
||||
| 不正な状態を表現不可能にする | 列挙型を使用して有効な状態のみをモデル化する |
|
||||
| `unwrap()` より `?` | エラーを伝播し、ライブラリ/本番コードでパニックしない |
|
||||
| 検証より解析 | 境界で非構造化データを型付き構造体に変換する |
|
||||
| 型安全のためのNewtype | 基本型をnewtypeでラップして引数の取り違えを防ぐ |
|
||||
| ループよりイテレーターを優先 | 宣言的なチェーンはより明確で通常より高速 |
|
||||
| Resultに `#[must_use]` を使用 | 呼び出し元が戻り値を処理することを保証する |
|
||||
| 柔軟な所有権のために `Cow` を使用 | 借用で十分な場合にアロケーションを避ける |
|
||||
| 完全マッチング | ビジネスクリティカルな列挙型でワイルドカード `_` を使わない |
|
||||
| `pub` インターフェースを最小化 | 内部APIには `pub(crate)` を使用 |
|
||||
|
||||
## 避けるべきアンチパターン
|
||||
|
||||
```rust
|
||||
// Bad: .unwrap() in production code
|
||||
let value = map.get("key").unwrap();
|
||||
|
||||
// Bad: .clone() to satisfy borrow checker without understanding why
|
||||
let data = expensive_data.clone();
|
||||
process(&original, &data);
|
||||
|
||||
// Bad: Using String when &str suffices
|
||||
fn greet(name: String) { /* should be &str */ }
|
||||
|
||||
// Bad: Box<dyn Error> in libraries (use thiserror instead)
|
||||
fn parse(input: &str) -> Result<Data, Box<dyn std::error::Error>> { todo!() }
|
||||
|
||||
// Bad: Ignoring must_use warnings
|
||||
let _ = validate(input); // Silently discarding a Result
|
||||
|
||||
// Bad: Blocking in async context
|
||||
async fn bad_async() {
|
||||
std::thread::sleep(Duration::from_secs(1)); // Blocks the executor!
|
||||
// Use: tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
}
|
||||
```
|
||||
|
||||
**覚えておくこと**:コンパイルが通れば、おそらく正しい——ただし `unwrap()` を避け、`unsafe` を最小化し、型システムを活用することが前提。
|
||||
502
docs/ja-JP/skills/rust-testing/SKILL.md
Normal file
502
docs/ja-JP/skills/rust-testing/SKILL.md
Normal file
@@ -0,0 +1,502 @@
|
||||
---
|
||||
name: rust-testing
|
||||
description: 単体テスト、統合テスト、非同期テスト、プロパティベーステスト、モック、カバレッジを含むRustテストパターン。TDD方法論に従う。
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Rust テストパターン
|
||||
|
||||
TDD方法論に従って信頼性が高く保守しやすいテストを書くための包括的なRustテストパターン。
|
||||
|
||||
## 使用場面
|
||||
|
||||
* 新しいRustの関数、メソッド、またはトレイトを書く場合
|
||||
* 既存のコードにテストカバレッジを追加する場合
|
||||
* パフォーマンスクリティカルなコードのベンチマークを作成する場合
|
||||
* 入力検証にプロパティベーステストを実装する場合
|
||||
* RustプロジェクトでTDDワークフローに従う場合
|
||||
|
||||
## 動作原理
|
||||
|
||||
1. **ターゲットコードを特定する** — テストする関数、トレイト、またはモジュールを見つける
|
||||
2. **テストを書く** — `#[cfg(test)]` モジュール内で `#[test]` を使用、rstest でパラメータ化テスト、または proptest でプロパティベーステスト
|
||||
3. **依存関係をモックする** — mockall を使用してテスト対象のユニットを分離する
|
||||
4. **テストを実行する (RED)** — テストが期待通りに失敗することを確認する
|
||||
5. **実装する (GREEN)** — テストを通過するための最小限のコードを書く
|
||||
6. **リファクタリングする** — テストを通過したまま、コードを改善する
|
||||
7. **カバレッジを確認する** — cargo-llvm-cov を使用し、80%以上を目標にする
|
||||
|
||||
## RustのTDDワークフロー
|
||||
|
||||
### RED-GREEN-REFACTOR サイクル
|
||||
|
||||
```
|
||||
RED → まず失敗するテストを書く
|
||||
GREEN → テストを通過する最小限のコードを書く
|
||||
REFACTOR → テストを通過したままコードをリファクタリングする
|
||||
REPEAT → 次の要件に進む
|
||||
```
|
||||
|
||||
### Rustでの段階的TDD
|
||||
|
||||
```rust
|
||||
// RED: Write test first, use todo!() as placeholder
|
||||
pub fn add(a: i32, b: i32) -> i32 { todo!() }
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn test_add() { assert_eq!(add(2, 3), 5); }
|
||||
}
|
||||
// cargo test → panics at 'not yet implemented'
|
||||
```
|
||||
|
||||
```rust
|
||||
// GREEN: Replace todo!() with minimal implementation
|
||||
pub fn add(a: i32, b: i32) -> i32 { a + b }
|
||||
// cargo test → PASS, then REFACTOR while keeping tests green
|
||||
```
|
||||
|
||||
## 単体テスト
|
||||
|
||||
### モジュールレベルのテスト整理
|
||||
|
||||
```rust
|
||||
// src/user.rs
|
||||
pub struct User {
|
||||
pub name: String,
|
||||
pub email: String,
|
||||
}
|
||||
|
||||
impl User {
|
||||
pub fn new(name: impl Into<String>, email: impl Into<String>) -> Result<Self, String> {
|
||||
let email = email.into();
|
||||
if !email.contains('@') {
|
||||
return Err(format!("invalid email: {email}"));
|
||||
}
|
||||
Ok(Self { name: name.into(), email })
|
||||
}
|
||||
|
||||
pub fn display_name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn creates_user_with_valid_email() {
|
||||
let user = User::new("Alice", "alice@example.com").unwrap();
|
||||
assert_eq!(user.display_name(), "Alice");
|
||||
assert_eq!(user.email, "alice@example.com");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_invalid_email() {
|
||||
let result = User::new("Bob", "not-an-email");
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("invalid email"));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### アサーションマクロ
|
||||
|
||||
```rust
|
||||
assert_eq!(2 + 2, 4); // Equality
|
||||
assert_ne!(2 + 2, 5); // Inequality
|
||||
assert!(vec![1, 2, 3].contains(&2)); // Boolean
|
||||
assert_eq!(value, 42, "expected 42 but got {value}"); // Custom message
|
||||
assert!((0.1_f64 + 0.2 - 0.3).abs() < f64::EPSILON); // Float comparison
|
||||
```
|
||||
|
||||
## エラーとパニックのテスト
|
||||
|
||||
### `Result` の戻り値のテスト
|
||||
|
||||
```rust
|
||||
#[test]
|
||||
fn parse_returns_error_for_invalid_input() {
|
||||
let result = parse_config("}{invalid");
|
||||
assert!(result.is_err());
|
||||
|
||||
// Assert specific error variant
|
||||
let err = result.unwrap_err();
|
||||
assert!(matches!(err, ConfigError::ParseError(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_succeeds_for_valid_input() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let config = parse_config(r#"{"port": 8080}"#)?;
|
||||
assert_eq!(config.port, 8080);
|
||||
Ok(()) // Test fails if any ? returns Err
|
||||
}
|
||||
```
|
||||
|
||||
### パニックのテスト
|
||||
|
||||
```rust
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn panics_on_empty_input() {
|
||||
process(&[]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "index out of bounds")]
|
||||
fn panics_with_specific_message() {
|
||||
let v: Vec<i32> = vec![];
|
||||
let _ = v[0];
|
||||
}
|
||||
```
|
||||
|
||||
## 統合テスト
|
||||
|
||||
### ファイル構造
|
||||
|
||||
```text
|
||||
my_crate/
|
||||
├── src/
|
||||
│ └── lib.rs
|
||||
├── tests/ # 統合テスト
|
||||
│ ├── api_test.rs # 各ファイルが独立したテストバイナリ
|
||||
│ ├── db_test.rs
|
||||
│ └── common/ # 共有テストユーティリティ
|
||||
│ └── mod.rs
|
||||
```
|
||||
|
||||
### 統合テストの書き方
|
||||
|
||||
```rust
|
||||
// tests/api_test.rs
|
||||
use my_crate::{App, Config};
|
||||
|
||||
#[test]
|
||||
fn full_request_lifecycle() {
|
||||
let config = Config::test_default();
|
||||
let app = App::new(config);
|
||||
|
||||
let response = app.handle_request("/health");
|
||||
assert_eq!(response.status, 200);
|
||||
assert_eq!(response.body, "OK");
|
||||
}
|
||||
```
|
||||
|
||||
## 非同期テスト
|
||||
|
||||
### Tokioの使用
|
||||
|
||||
```rust
|
||||
#[tokio::test]
|
||||
async fn fetches_data_successfully() {
|
||||
let client = TestClient::new().await;
|
||||
let result = client.get("/data").await;
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap().items.len(), 3);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn handles_timeout() {
|
||||
use std::time::Duration;
|
||||
let result = tokio::time::timeout(
|
||||
Duration::from_millis(100),
|
||||
slow_operation(),
|
||||
).await;
|
||||
|
||||
assert!(result.is_err(), "should have timed out");
|
||||
}
|
||||
```
|
||||
|
||||
## テスト整理パターン
|
||||
|
||||
### `rstest` を使用したパラメータ化テスト
|
||||
|
||||
```rust
|
||||
use rstest::{rstest, fixture};
|
||||
|
||||
#[rstest]
|
||||
#[case("hello", 5)]
|
||||
#[case("", 0)]
|
||||
#[case("rust", 4)]
|
||||
fn test_string_length(#[case] input: &str, #[case] expected: usize) {
|
||||
assert_eq!(input.len(), expected);
|
||||
}
|
||||
|
||||
// Fixtures
|
||||
#[fixture]
|
||||
fn test_db() -> TestDb {
|
||||
TestDb::new_in_memory()
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_insert(test_db: TestDb) {
|
||||
test_db.insert("key", "value");
|
||||
assert_eq!(test_db.get("key"), Some("value".into()));
|
||||
}
|
||||
```
|
||||
|
||||
### テストヘルパー関数
|
||||
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Creates a test user with sensible defaults.
|
||||
fn make_user(name: &str) -> User {
|
||||
User::new(name, &format!("{name}@test.com")).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_display() {
|
||||
let user = make_user("alice");
|
||||
assert_eq!(user.display_name(), "alice");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## `proptest` を使用したプロパティベーステスト
|
||||
|
||||
### 基本的なプロパティテスト
|
||||
|
||||
```rust
|
||||
use proptest::prelude::*;
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn encode_decode_roundtrip(input in ".*") {
|
||||
let encoded = encode(&input);
|
||||
let decoded = decode(&encoded).unwrap();
|
||||
assert_eq!(input, decoded);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sort_preserves_length(mut vec in prop::collection::vec(any::<i32>(), 0..100)) {
|
||||
let original_len = vec.len();
|
||||
vec.sort();
|
||||
assert_eq!(vec.len(), original_len);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sort_produces_ordered_output(mut vec in prop::collection::vec(any::<i32>(), 0..100)) {
|
||||
vec.sort();
|
||||
for window in vec.windows(2) {
|
||||
assert!(window[0] <= window[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### カスタムストラテジー
|
||||
|
||||
```rust
|
||||
use proptest::prelude::*;
|
||||
|
||||
fn valid_email() -> impl Strategy<Value = String> {
|
||||
("[a-z]{1,10}", "[a-z]{1,5}")
|
||||
.prop_map(|(user, domain)| format!("{user}@{domain}.com"))
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn accepts_valid_emails(email in valid_email()) {
|
||||
assert!(User::new("Test", &email).is_ok());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## `mockall` を使用したモック
|
||||
|
||||
### トレイトベースのモック
|
||||
|
||||
```rust
|
||||
use mockall::{automock, predicate::eq};
|
||||
|
||||
#[automock]
|
||||
trait UserRepository {
|
||||
fn find_by_id(&self, id: u64) -> Option<User>;
|
||||
fn save(&self, user: &User) -> Result<(), StorageError>;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn service_returns_user_when_found() {
|
||||
let mut mock = MockUserRepository::new();
|
||||
mock.expect_find_by_id()
|
||||
.with(eq(42))
|
||||
.times(1)
|
||||
.returning(|_| Some(User { id: 42, name: "Alice".into() }));
|
||||
|
||||
let service = UserService::new(Box::new(mock));
|
||||
let user = service.get_user(42).unwrap();
|
||||
assert_eq!(user.name, "Alice");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn service_returns_none_when_not_found() {
|
||||
let mut mock = MockUserRepository::new();
|
||||
mock.expect_find_by_id()
|
||||
.returning(|_| None);
|
||||
|
||||
let service = UserService::new(Box::new(mock));
|
||||
assert!(service.get_user(99).is_none());
|
||||
}
|
||||
```
|
||||
|
||||
## ドキュメントテスト
|
||||
|
||||
### 実行可能なドキュメント
|
||||
|
||||
````rust
|
||||
/// Adds two numbers together.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use my_crate::add;
|
||||
///
|
||||
/// assert_eq!(add(2, 3), 5);
|
||||
/// assert_eq!(add(-1, 1), 0);
|
||||
/// ```
|
||||
pub fn add(a: i32, b: i32) -> i32 {
|
||||
a + b
|
||||
}
|
||||
|
||||
/// Parses a config string.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns `Err` if the input is not valid TOML.
|
||||
///
|
||||
/// ```no_run
|
||||
/// use my_crate::parse_config;
|
||||
///
|
||||
/// let config = parse_config(r#"port = 8080"#).unwrap();
|
||||
/// assert_eq!(config.port, 8080);
|
||||
/// ```
|
||||
///
|
||||
/// ```no_run
|
||||
/// use my_crate::parse_config;
|
||||
///
|
||||
/// assert!(parse_config("}{invalid").is_err());
|
||||
/// ```
|
||||
pub fn parse_config(input: &str) -> Result<Config, ParseError> {
|
||||
todo!()
|
||||
}
|
||||
````
|
||||
|
||||
## Criterionを使用したベンチマーク
|
||||
|
||||
```toml
|
||||
# Cargo.toml
|
||||
[dev-dependencies]
|
||||
criterion = { version = "0.5", features = ["html_reports"] }
|
||||
|
||||
[[bench]]
|
||||
name = "benchmark"
|
||||
harness = false
|
||||
```
|
||||
|
||||
```rust
|
||||
// benches/benchmark.rs
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
|
||||
fn fibonacci(n: u64) -> u64 {
|
||||
match n {
|
||||
0 | 1 => n,
|
||||
_ => fibonacci(n - 1) + fibonacci(n - 2),
|
||||
}
|
||||
}
|
||||
|
||||
fn bench_fibonacci(c: &mut Criterion) {
|
||||
c.bench_function("fib 20", |b| b.iter(|| fibonacci(black_box(20))));
|
||||
}
|
||||
|
||||
criterion_group!(benches, bench_fibonacci);
|
||||
criterion_main!(benches);
|
||||
```
|
||||
|
||||
## テストカバレッジ
|
||||
|
||||
### カバレッジの実行
|
||||
|
||||
```bash
|
||||
# Install: cargo install cargo-llvm-cov (or use taiki-e/install-action in CI)
|
||||
cargo llvm-cov # Summary
|
||||
cargo llvm-cov --html # HTML report
|
||||
cargo llvm-cov --lcov > lcov.info # LCOV format for CI
|
||||
cargo llvm-cov --fail-under-lines 80 # Fail if below threshold
|
||||
```
|
||||
|
||||
### カバレッジ目標
|
||||
|
||||
| コードの種類 | 目標 |
|
||||
|-----------|--------|
|
||||
| クリティカルなビジネスロジック | 100% |
|
||||
| パブリックAPI | 90%以上 |
|
||||
| 汎用コード | 80%以上 |
|
||||
| 生成済み / FFIバインディング | 除外 |
|
||||
|
||||
## テストコマンド
|
||||
|
||||
```bash
|
||||
cargo test # Run all tests
|
||||
cargo test -- --nocapture # Show println output
|
||||
cargo test test_name # Run tests matching pattern
|
||||
cargo test --lib # Unit tests only
|
||||
cargo test --test api_test # Integration tests only
|
||||
cargo test --doc # Doc tests only
|
||||
cargo test --no-fail-fast # Don't stop on first failure
|
||||
cargo test -- --ignored # Run ignored tests
|
||||
```
|
||||
|
||||
## ベストプラクティス
|
||||
|
||||
**すべきこと:**
|
||||
|
||||
* まずテストを書く (TDD)
|
||||
* 単体テストには `#[cfg(test)]` モジュールを使用する
|
||||
* 実装ではなく動作をテストする
|
||||
* シナリオを説明する記述的なテスト名を使用する
|
||||
* より良いエラーメッセージのために `assert!` より `assert_eq!` を優先する
|
||||
* クリーンなエラー出力のために `Result` を返すテストでは `?` を使用する
|
||||
* テストを独立させる——共有の可変状態なし
|
||||
|
||||
**すべきでないこと:**
|
||||
|
||||
* `Result::is_err()` をテストできる場合に `#[should_panic]` を使用する
|
||||
* すべてをモックする——可能なら統合テストを優先する
|
||||
* フレーキーなテストを無視する——修正または分離する
|
||||
* テストで `sleep()` を使用する——チャンネル、バリア、または `tokio::time::pause()` を使用する
|
||||
* エラーパスのテストをスキップする
|
||||
|
||||
## CI統合
|
||||
|
||||
```yaml
|
||||
# GitHub Actions
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: clippy, rustfmt
|
||||
|
||||
- name: Check formatting
|
||||
run: cargo fmt --check
|
||||
|
||||
- name: Clippy
|
||||
run: cargo clippy -- -D warnings
|
||||
|
||||
- name: Run tests
|
||||
run: cargo test
|
||||
|
||||
- uses: taiki-e/install-action@cargo-llvm-cov
|
||||
|
||||
- name: Coverage
|
||||
run: cargo llvm-cov --fail-under-lines 80
|
||||
```
|
||||
|
||||
**覚えておくこと**:テストはドキュメントである。コードをどのように使うべきかを示している。明確に書き、最新の状態を保つこと。
|
||||
60
docs/ja-JP/skills/skill-comply/SKILL.md
Normal file
60
docs/ja-JP/skills/skill-comply/SKILL.md
Normal file
@@ -0,0 +1,60 @@
|
||||
---
|
||||
name: skill-comply
|
||||
description: スキル、ルール、エージェント定義が実際に遵守されているかを可視化する——3種類のプロンプト厳格度レベルのシナリオを自動生成し、エージェントを実行し、動作シーケンスを分類し、完全なツール呼び出しタイムラインの遵守率をレポートする
|
||||
origin: ECC
|
||||
tools: Read, Bash
|
||||
---
|
||||
|
||||
# skill-comply:自動化された遵守測定
|
||||
|
||||
コーディングエージェントがスキル、ルール、またはエージェント定義を実際に遵守しているかを以下の方法で測定する:
|
||||
|
||||
1. 任意の .md ファイルから期待される動作シーケンス(仕様)を自動生成する
|
||||
2. プロンプトの厳格度が段階的に低下するシナリオを自動生成する(支持的 → 中立的 → 競合的)
|
||||
3. `claude -p` を実行し、stream-json 経由でツール呼び出しトレースを取得する
|
||||
4. 正規表現ではなくLLMを使用してツール呼び出しを仕様ステップに分類する
|
||||
5. 決定論的に時系列順を確認する
|
||||
6. 仕様、プロンプト、タイムラインを含む自己完結型レポートを生成する
|
||||
|
||||
## サポートされるターゲット
|
||||
|
||||
* **スキル**(`skills/*/SKILL.md`):検索優先、TDDガイドなどのワークフロースキル
|
||||
* **ルール**(`rules/common/*.md`):testing.md、security.md、git-workflow.md などの強制的なルール
|
||||
* **エージェント定義**(`agents/*.md`):エージェントが期待される場面で呼び出されるか(内部ワークフロー検証は未サポート)
|
||||
|
||||
## 起動条件
|
||||
|
||||
* ユーザーが `/skill-comply <path>` を実行する
|
||||
* ユーザーが「このルールは本当に遵守されているか?」と尋ねる
|
||||
* 新しいルール/スキルを追加した後、エージェントの遵守を確認する
|
||||
* 品質メンテナンスの一環として定期的に実行する
|
||||
|
||||
## 使い方
|
||||
|
||||
```bash
|
||||
# Full run
|
||||
uv run python -m scripts.run ~/.claude/rules/common/testing.md
|
||||
|
||||
# Dry run (no cost, spec + scenarios only)
|
||||
uv run python -m scripts.run --dry-run ~/.claude/skills/search-first/SKILL.md
|
||||
|
||||
# Custom models
|
||||
uv run python -m scripts.run --gen-model haiku --model sonnet <path>
|
||||
```
|
||||
|
||||
## 重要なコンセプト:プロンプト独立性
|
||||
|
||||
プロンプトが明示的にサポートしていない場合でも、スキル/ルールが遵守されるかどうかを測定する。
|
||||
|
||||
## レポートの内容
|
||||
|
||||
レポートは自己完結型で、以下を含む:
|
||||
|
||||
1. 期待される動作シーケンス(自動生成された仕様)
|
||||
2. シナリオプロンプト(各厳格度レベルで尋ねる内容)
|
||||
3. 各シナリオの遵守スコア
|
||||
4. LLM分類ラベル付きのツール呼び出しタイムライン
|
||||
|
||||
### 高度な内容(オプション)
|
||||
|
||||
フックに精通したユーザー向けに、レポートには遵守率が低いステップに対するフック強化の推奨事項も含まれる。これは参考情報——主要な価値は遵守性自体の可視化にある。
|
||||
194
docs/ja-JP/skills/skill-stocktake/SKILL.md
Normal file
194
docs/ja-JP/skills/skill-stocktake/SKILL.md
Normal file
@@ -0,0 +1,194 @@
|
||||
---
|
||||
name: skill-stocktake
|
||||
description: "Claudeのスキルとコマンドの品質を監査するためのツール。変更されたスキルのみを対象とした高速スキャンと、順次サブエージェントバッチ評価を使用した完全棚卸しモードをサポートする。"
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# skill-stocktake
|
||||
|
||||
品質チェックリスト + AI全体判断を使用して、すべてのClaudeスキルとコマンドを審査するスラッシュコマンド(`/skill-stocktake`)。2つのモードをサポートする:最近変更されたスキルの高速スキャンと、完全レビューのための完全棚卸し。
|
||||
|
||||
## スコープ
|
||||
|
||||
このコマンドは、**コマンドを呼び出したディレクトリを基準とした**以下のパスを対象とする:
|
||||
|
||||
| パス | 説明 |
|
||||
|------|-------------|
|
||||
| `~/.claude/skills/` | グローバルスキル(全プロジェクト) |
|
||||
| `{cwd}/.claude/skills/` | プロジェクトレベルのスキル(ディレクトリが存在する場合) |
|
||||
|
||||
**フェーズ1の開始時に、コマンドはどのパスが見つかりスキャンされたかを明示的にリストアップする。**
|
||||
|
||||
### 特定のプロジェクトをターゲットにする
|
||||
|
||||
プロジェクトレベルのスキルを含めるには、そのプロジェクトのルートから実行する:
|
||||
|
||||
```bash
|
||||
cd ~/path/to/my-project
|
||||
/skill-stocktake
|
||||
```
|
||||
|
||||
プロジェクトに `.claude/skills/` ディレクトリがない場合、グローバルスキルとコマンドのみが評価される。
|
||||
|
||||
## モード
|
||||
|
||||
| モード | トリガー条件 | 所要時間 |
|
||||
|------|---------|---------|
|
||||
| 高速スキャン | `results.json` が存在する(デフォルト) | 5〜10分 |
|
||||
| 完全棚卸し | `results.json` が存在しない、または `/skill-stocktake full` | 20〜30分 |
|
||||
|
||||
**結果キャッシュ:** `~/.claude/skills/skill-stocktake/results.json`
|
||||
|
||||
## 高速スキャンフロー
|
||||
|
||||
前回の実行以降に変更されたスキルのみを再評価する(5〜10分)。
|
||||
|
||||
1. `~/.claude/skills/skill-stocktake/results.json` を読み取る
|
||||
2. 実行する:`bash ~/.claude/skills/skill-stocktake/scripts/quick-diff.sh \ ~/.claude/skills/skill-stocktake/results.json`
|
||||
(プロジェクトディレクトリは `$PWD/.claude/skills` から自動検出。必要な場合のみ明示的に渡す)
|
||||
3. 出力が `[]` の場合:「前回の実行以降に変更なし。」とレポートして停止する
|
||||
4. 変更されたファイルのみを同じフェーズ2の基準で再評価する
|
||||
5. 前回の結果から変更されていないスキルを引き継ぐ
|
||||
6. 差分のみを出力する
|
||||
7. 実行する:`bash ~/.claude/skills/skill-stocktake/scripts/save-results.sh \ ~/.claude/skills/skill-stocktake/results.json <<< "$EVAL_RESULTS"`
|
||||
|
||||
## 完全棚卸しフロー
|
||||
|
||||
### フェーズ 1 — インベントリ
|
||||
|
||||
実行する:`bash ~/.claude/skills/skill-stocktake/scripts/scan.sh`
|
||||
|
||||
スクリプトはスキルファイルを列挙し、フロントマターを抽出し、UTC修正時刻を収集する。
|
||||
プロジェクトディレクトリは `$PWD/.claude/skills` から自動検出。必要な場合のみ明示的に渡す。
|
||||
スクリプト出力からスキャンサマリーとインベントリテーブルを表示する:
|
||||
|
||||
```
|
||||
スキャン中:
|
||||
✓ ~/.claude/skills/ (17 個のファイル)
|
||||
✗ {cwd}/.claude/skills/ (見つからない — グローバルスキルのみ)
|
||||
```
|
||||
|
||||
| スキル | 7日間使用 | 30日間使用 | 説明 |
|
||||
|-------|--------|---------|-------------|
|
||||
|
||||
### フェーズ 2 — 品質評価
|
||||
|
||||
完全なインベントリとチェック項目を含む**汎用エージェント**ツールのサブエージェントを起動する:
|
||||
|
||||
```text
|
||||
Agent(
|
||||
subagent_type="general-purpose",
|
||||
prompt="
|
||||
チェックリストに基づいて以下のスキルインベントリを評価してください。
|
||||
|
||||
[INVENTORY]
|
||||
|
||||
[CHECKLIST]
|
||||
|
||||
各スキルについてJSONを返してください:
|
||||
{ \"verdict\": \"Keep\"|\"Improve\"|\"Update\"|\"Retire\"|\"Merge into [X]\", \"reason\": \"...\" }
|
||||
"
|
||||
)
|
||||
```
|
||||
|
||||
サブエージェントは各スキルを読み取り、チェック項目を適用し、各スキルのJSON結果を返す:
|
||||
|
||||
`{ "verdict": "Keep"|"Improve"|"Update"|"Retire"|"Merge into [X]", "reason": "..." }`
|
||||
|
||||
**チャンク指針:** 各サブエージェント呼び出しは約20個のスキルを処理し、コンテキストを管理可能に保つ。各チャンクの後、中間結果を `results.json` に保存する(`status: "in_progress"`)。
|
||||
|
||||
全スキルの評価が完了したら:`status: "completed"` を設定し、フェーズ3に進む。
|
||||
|
||||
**再開検出:** 起動時に `status: "in_progress"` が見つかった場合、最初の未評価スキルから再開する。
|
||||
|
||||
各スキルはこのチェックリストに基づいて評価される:
|
||||
|
||||
```
|
||||
- [ ] 他のスキルとの内容の重複を確認済み
|
||||
- [ ] MEMORY.md / CLAUDE.md との重複を確認済み
|
||||
- [ ] 技術的参照の時効性を確認済み(ツール名 / CLI引数 / APIが存在する場合、WebSearchで検証)
|
||||
- [ ] 使用頻度を考慮済み
|
||||
```
|
||||
|
||||
判定基準:
|
||||
|
||||
| 判定 | 意味 |
|
||||
|---------|---------|
|
||||
| Keep | 有用かつ最新 |
|
||||
| Improve | 保持する価値があるが、特定の改善が必要 |
|
||||
| Update | 参照された技術が古い(WebSearchで検証) |
|
||||
| Retire | 品質が低い、陳腐化、またはコストが非対称 |
|
||||
| Merge into \[X] | 別のスキルと大幅に重複している。マージターゲットを命名する |
|
||||
|
||||
評価は**AI全体判断**——数値スコアリングルーブリックではない。指針となる次元:
|
||||
|
||||
* **実行可能性**:即座に行動できるコード例、コマンド、または手順
|
||||
* **スコープの適合性**:名前、トリガー、内容が一致している。広すぎず、狭すぎない
|
||||
* **独自性**:MEMORY.md / CLAUDE.md / 他のスキルで代替できない価値
|
||||
* **時効性**:技術的参照が現在の環境で有効
|
||||
|
||||
**理由の品質要件** — `reason` フィールドは自己完結型で意思決定を支えられる必要がある:
|
||||
|
||||
* 単に「変更なし」と書かない——常に核心的な証拠を再述する
|
||||
* **Retire** の場合:(1) 発見された具体的な欠陥、(2) 同じニーズをカバーする代替案を述べる
|
||||
* 悪:`"Superseded"`
|
||||
* 良:`"disable-model-invocation: true already set; superseded by continuous-learning-v2 which covers all the same patterns plus confidence scoring. No unique content remains."`
|
||||
* **Merge** の場合:ターゲットを命名し、何を統合するかを説明する
|
||||
* 悪:`"Overlaps with X"`
|
||||
* 良:`"42-line thin content; Step 4 of chatlog-to-article already covers the same workflow. Integrate the 'article angle' tip as a note in that skill."`
|
||||
* **Improve** の場合:必要な具体的な変更を説明する(どのセクション、何の操作、該当する場合は目標サイズ)
|
||||
* 悪:`"Too long"`
|
||||
* 良:`"276 lines; Section 'Framework Comparison' (L80–140) duplicates ai-era-architecture-principles; delete it to reach ~150 lines."`
|
||||
* **Keep**(高速スキャンでmtimeのみ変更の場合):元の判定理由を再述し、「変更なし」と書かない
|
||||
* 悪:`"Unchanged"`
|
||||
* 良:`"mtime updated but content unchanged. Unique Python reference explicitly imported by rules/python/; no overlap found."`
|
||||
|
||||
### フェーズ 3 — サマリーテーブル
|
||||
|
||||
| スキル | 7日間使用 | 判定 | 理由 |
|
||||
|-------|--------|---------|--------|
|
||||
|
||||
### フェーズ 4 — 統合
|
||||
|
||||
1. **Retire / Merge**:ユーザーの確認前に、ファイルごとに詳細な理由を提示する:
|
||||
* 発見された具体的な問題(重複、陳腐化、リンク切れなど)
|
||||
* 同じ機能をカバーする代替案(Retire の場合:どの既存スキル/ルール;Merge の場合:ターゲットファイルと何を統合するか)
|
||||
* 削除の影響(依存するスキル、MEMORY.md 参照、影響を受けるワークフローがあるか)
|
||||
2. **Improve**:具体的な改善提案と理由を提示する:
|
||||
* 何を変更し、なぜか(例:「X/Yセクションが python-patterns と重複しているため、430行を200行に圧縮する」)
|
||||
* ユーザーが行動するかどうかを決定する
|
||||
3. **Update**:確認したソースから更新されたコンテンツを提示する
|
||||
4. MEMORY.md の行数を確認し、100行を超えている場合は圧縮を提案する
|
||||
|
||||
## 結果ファイルスキーマ
|
||||
|
||||
`~/.claude/skills/skill-stocktake/results.json`:
|
||||
|
||||
**`evaluated_at`**:評価が完了した実際のUTC時刻を設定する必要がある。
|
||||
Bash で取得する:`date -u +%Y-%m-%dT%H:%M:%SZ`。`T00:00:00Z` のような日付のみの近似値は絶対に使わない。
|
||||
|
||||
```json
|
||||
{
|
||||
"evaluated_at": "2026-02-21T10:00:00Z",
|
||||
"mode": "full",
|
||||
"batch_progress": {
|
||||
"total": 80,
|
||||
"evaluated": 80,
|
||||
"status": "completed"
|
||||
},
|
||||
"skills": {
|
||||
"skill-name": {
|
||||
"path": "~/.claude/skills/skill-name/SKILL.md",
|
||||
"verdict": "Keep",
|
||||
"reason": "Concrete, actionable, unique value for X workflow",
|
||||
"mtime": "2026-01-15T08:30:00Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 注意事項
|
||||
|
||||
* 評価はブラインド:ソース(ECC、自作、自動抽出)に関わらず、すべてのスキルに同じチェックリストを適用する
|
||||
* アーカイブ/削除操作は常に明示的なユーザー確認が必要
|
||||
* スキルのソースによって判定を分岐させない
|
||||
154
docs/ja-JP/skills/social-graph-ranker/SKILL.md
Normal file
154
docs/ja-JP/skills/social-graph-ranker/SKILL.md
Normal file
@@ -0,0 +1,154 @@
|
||||
---
|
||||
name: social-graph-ranker
|
||||
description: XとLinkedInでのウォームイントロ発見、ブリッジスコアリング、ネットワークギャップ分析のための重み付きソーシャルグラフランキング。ユーザーがランキングエンジン自体を必要としている場合(より広いプロモーションやネットワーク維持ワークフローではなく)に使用する。
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# ソーシャルグラフランカー
|
||||
|
||||
ネットワーク認識型アウトリーチのための正規化された重み付きグラフランキングレイヤー。
|
||||
|
||||
以下の機能が必要な場合にこのツールを使用する:
|
||||
|
||||
* 内在的価値に基づいて既存の相互フォロワーまたはコネクションをランク付けする
|
||||
* ターゲットリストに対してウォームパスをマッピングする
|
||||
* 1度と2度のコネクション全体でブリッジ価値を測定する
|
||||
* ウォームな紹介とコールドアウトリーチのどちらが適切かを判断する
|
||||
* `lead-intelligence` や `connections-optimizer` とは独立してグラフの数学的原理を理解する
|
||||
|
||||
## 単独での使用場面
|
||||
|
||||
ユーザーが主にランキングエンジンを必要としている場合にこのスキルを選択する:
|
||||
|
||||
* 「私のネットワークで誰が最もよい紹介をしてくれるか?」
|
||||
* 「相互フォロワーをランク付けして、この人たちへの連絡を手伝ってもらえる人を見つける」
|
||||
* 「このICPに対して私のグラフをマッピングする」
|
||||
* 「ブリッジの数学的計算を見せる」
|
||||
|
||||
ユーザーが実際に以下を必要としている場合は、単独で使用しない:
|
||||
|
||||
* 完全なリード生成とアウトリーチシーケンス -> `lead-intelligence` を使用
|
||||
* ネットワークのトリミング、再バランシング、拡張 -> `connections-optimizer` を使用
|
||||
|
||||
## 入力
|
||||
|
||||
以下を収集または推論する:
|
||||
|
||||
* ターゲットとなる人物、企業、またはICP定義
|
||||
* XまたはLinkedIn、あるいは両方におけるユーザーの現在のグラフ
|
||||
* 役割、業界、地理、レスポンス性などの重み付け優先度
|
||||
* 探索の深さと減衰の許容度
|
||||
|
||||
## コアモデル
|
||||
|
||||
以下が与えられたとする:
|
||||
|
||||
* `T` = 重み付きターゲットのセット
|
||||
* `M` = 現在の相互フォロワー/直接コネクション
|
||||
* `d(m, t)` = 相互フォロワー `m` からターゲット `t` への最短ホップ距離
|
||||
* `w(t)` = シグナルスコアリングからのターゲット重み
|
||||
|
||||
基本ブリッジスコア:
|
||||
|
||||
```text
|
||||
B(m) = Σ_{t ∈ T} w(t) · λ^(d(m,t) - 1)
|
||||
```
|
||||
|
||||
ここで:
|
||||
|
||||
* `λ` は減衰因子、通常 `0.5`
|
||||
* 直接パスは全価値を提供
|
||||
* ホップが増えるごとに貢献が半分になる
|
||||
|
||||
2度拡張:
|
||||
|
||||
```text
|
||||
B_ext(m) = B(m) + α · Σ_{m' ∈ N(m) \\ M} Σ_{t ∈ T} w(t) · λ^(d(m',t))
|
||||
```
|
||||
|
||||
ここで:
|
||||
|
||||
* `N(m) \\ M` は相互フォロワーが知っているがユーザーが知らない人のセット
|
||||
* `α` は2度の到達可能性に対する割引、通常 `0.3`
|
||||
|
||||
レスポンス調整後の最終ランキング:
|
||||
|
||||
```text
|
||||
R(m) = B_ext(m) · (1 + β · engagement(m))
|
||||
```
|
||||
|
||||
ここで:
|
||||
|
||||
* `engagement(m)` は正規化されたレスポンス性または関係強度
|
||||
* `β` はエンゲージメントボーナス、通常 `0.2`
|
||||
|
||||
解釈:
|
||||
|
||||
* 第1層:高い `R(m)` と直接ブリッジパス -> ウォームな紹介リクエスト
|
||||
* 第2層:中程度の `R(m)` と1ホップのブリッジパス -> 条件付き紹介リクエスト
|
||||
* 第3層:低い `R(m)` またはブリッジなし -> 直接アウトリーチまたはギャップ補完に注力
|
||||
|
||||
## スコアリングシグナル
|
||||
|
||||
グラフ探索前に、現在の優先度セットに基づいてターゲットを重み付けする:
|
||||
|
||||
* 役職または職位の一致度
|
||||
* 企業または業界の適合性
|
||||
* 現在のアクティビティと時効性
|
||||
* 地理的な関連性
|
||||
* 影響力またはリーチ
|
||||
* レスポンスの可能性
|
||||
|
||||
探索後に相互フォロワーを重み付けする:
|
||||
|
||||
* ターゲットセットへの重み付きパスの数
|
||||
* それらのパスの直接性
|
||||
* レスポンス性または過去のインタラクション履歴
|
||||
* 紹介を行うためのコンテキスト適合性
|
||||
|
||||
## ワークフロー
|
||||
|
||||
1. 重み付きターゲットセットを構築する。
|
||||
2. X、LinkedIn、または両方からユーザーのグラフを取得する。
|
||||
3. 直接ブリッジスコアを計算する。
|
||||
4. 最も価値の高い相互フォロワーの2度の候補を拡張する。
|
||||
5. `R(m)` でランク付けする。
|
||||
6. 以下を返す:
|
||||
* 最良のウォームな紹介リクエスト
|
||||
* 条件付きブリッジパス
|
||||
* ウォームパスが存在しないグラフギャップ
|
||||
|
||||
## 出力フォーマット
|
||||
|
||||
```text
|
||||
ソーシャルグラフランキング
|
||||
====================
|
||||
|
||||
優先度セット:
|
||||
プラットフォーム:
|
||||
減衰モデル:
|
||||
|
||||
トップブリッジ
|
||||
- 相互フォロワー / コネクション
|
||||
基本スコア:
|
||||
拡張スコア:
|
||||
最良ターゲット:
|
||||
パスサマリー:
|
||||
推奨アクション:
|
||||
|
||||
条件付きパス
|
||||
- 相互フォロワー / コネクション
|
||||
理由:
|
||||
追加ホップコスト:
|
||||
|
||||
ウォームパスなし
|
||||
- ターゲット
|
||||
推奨:直接連絡 / グラフギャップを補完
|
||||
```
|
||||
|
||||
## 関連スキル
|
||||
|
||||
* `lead-intelligence` はより広いターゲット発見とアウトリーチパイプラインでこのランキングモデルを使用する
|
||||
* `connections-optimizer` は誰を保持、トリミング、または追加するかを決定する際に同じブリッジロジックを使用する
|
||||
* `brand-voice` は紹介リクエストや直接アウトリーチを起草する前に実行する
|
||||
* `x-api` はXグラフへのアクセスとオプションの実行パスを提供する
|
||||
143
docs/ja-JP/skills/swift-actor-persistence/SKILL.md
Normal file
143
docs/ja-JP/skills/swift-actor-persistence/SKILL.md
Normal file
@@ -0,0 +1,143 @@
|
||||
---
|
||||
name: swift-actor-persistence
|
||||
description: Swiftでactorを使用してスレッドセーフなデータ永続化を実装する——メモリキャッシュとファイルバックドストレージを組み合わせ、設計によってデータ競合を排除する。
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# スレッドセーフな永続化のための Swift Actor
|
||||
|
||||
Swiftのactorを使用してスレッドセーフなデータ永続化レイヤーを構築するパターン。メモリキャッシュとファイルバックドストレージを組み合わせ、actorモデルを活用してコンパイル時にデータ競合を排除する。
|
||||
|
||||
## 起動条件
|
||||
|
||||
* Swift 5.5以降でデータ永続化レイヤーを構築する場合
|
||||
* 共有可変状態へのスレッドセーフアクセスが必要な場合
|
||||
* 手動の同期(ロック、DispatchQueue)を排除したい場合
|
||||
* ローカルストレージを持つオフラインファースとアプリを構築する場合
|
||||
|
||||
## コアパターン
|
||||
|
||||
### Actorベースのリポジトリ
|
||||
|
||||
Actorモデルはシリアライズされたアクセスを保証する——コンパイラによって強制されるデータ競合なし。
|
||||
|
||||
```swift
|
||||
public actor LocalRepository<T: Codable & Identifiable> where T.ID == String {
|
||||
private var cache: [String: T] = [:]
|
||||
private let fileURL: URL
|
||||
|
||||
public init(directory: URL = .documentsDirectory, filename: String = "data.json") {
|
||||
self.fileURL = directory.appendingPathComponent(filename)
|
||||
// Synchronous load during init (actor isolation not yet active)
|
||||
self.cache = Self.loadSynchronously(from: fileURL)
|
||||
}
|
||||
|
||||
// MARK: - Public API
|
||||
|
||||
public func save(_ item: T) throws {
|
||||
cache[item.id] = item
|
||||
try persistToFile()
|
||||
}
|
||||
|
||||
public func delete(_ id: String) throws {
|
||||
cache[id] = nil
|
||||
try persistToFile()
|
||||
}
|
||||
|
||||
public func find(by id: String) -> T? {
|
||||
cache[id]
|
||||
}
|
||||
|
||||
public func loadAll() -> [T] {
|
||||
Array(cache.values)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func persistToFile() throws {
|
||||
let data = try JSONEncoder().encode(Array(cache.values))
|
||||
try data.write(to: fileURL, options: .atomic)
|
||||
}
|
||||
|
||||
private static func loadSynchronously(from url: URL) -> [String: T] {
|
||||
guard let data = try? Data(contentsOf: url),
|
||||
let items = try? JSONDecoder().decode([T].self, from: data) else {
|
||||
return [:]
|
||||
}
|
||||
return Dictionary(uniqueKeysWithValues: items.map { ($0.id, $0) })
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 使い方
|
||||
|
||||
Actorの分離により、すべての呼び出しは自動的に非同期になる:
|
||||
|
||||
```swift
|
||||
let repository = LocalRepository<Question>()
|
||||
|
||||
// Read — fast O(1) lookup from in-memory cache
|
||||
let question = await repository.find(by: "q-001")
|
||||
let allQuestions = await repository.loadAll()
|
||||
|
||||
// Write — updates cache and persists to file atomically
|
||||
try await repository.save(newQuestion)
|
||||
try await repository.delete("q-001")
|
||||
```
|
||||
|
||||
### @Observable ViewModel との組み合わせ
|
||||
|
||||
```swift
|
||||
@Observable
|
||||
final class QuestionListViewModel {
|
||||
private(set) var questions: [Question] = []
|
||||
private let repository: LocalRepository<Question>
|
||||
|
||||
init(repository: LocalRepository<Question> = LocalRepository()) {
|
||||
self.repository = repository
|
||||
}
|
||||
|
||||
func load() async {
|
||||
questions = await repository.loadAll()
|
||||
}
|
||||
|
||||
func add(_ question: Question) async throws {
|
||||
try await repository.save(question)
|
||||
questions = await repository.loadAll()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 重要な設計上の決定
|
||||
|
||||
| 決定 | 理由 |
|
||||
|----------|-----------|
|
||||
| Actorを使用(クラス + ロックではなく) | コンパイラによって強制されるスレッド安全性、手動同期不要 |
|
||||
| メモリキャッシュ + ファイル永続化 | キャッシュからの高速読み取り、ディスクへの永続的な書き込み |
|
||||
| 初期化時の同期ロード | 非同期初期化の複雑さを回避 |
|
||||
| IDをキーとする辞書 | 識別子によるO(1)検索 |
|
||||
| ジェネリック `Codable & Identifiable` | あらゆるモデル型で再利用可能 |
|
||||
| アトミックなファイル書き込み(`.atomic`) | クラッシュ時の部分書き込みを防ぐ |
|
||||
|
||||
## ベストプラクティス
|
||||
|
||||
* **Actorの境界を越えるすべてのデータに `Sendable` 型を使用する**
|
||||
* **Actorのパブリックなアビリティを最小化する** —— 永続化の詳細ではなく、ドメイン操作のみを公開する
|
||||
* **`.atomic` 書き込みを使用する** —— 書き込み中のアプリクラッシュによるデータ破損を防ぐ
|
||||
* **`init` で同期的にロードする** —— 非同期イニシャライザはローカルファイルに対するわずかな利点のために複雑さが増す
|
||||
* **`@Observable` ViewModelと組み合わせる** —— リアクティブなUI更新を実現する
|
||||
|
||||
## 避けるべきアンチパターン
|
||||
|
||||
* Swiftの新しい並行処理コードでActorの代わりに `DispatchQueue` または `NSLock` を使用する
|
||||
* 内部のキャッシュ辞書を外部の呼び出し元に公開する
|
||||
* 検証なしでファイルURLを設定可能にする
|
||||
* すべてのActor メソッド呼び出しが `await` であることを忘れる——呼び出し元は非同期コンテキストを処理する必要がある
|
||||
* Actor の分離をバイパスするために `nonisolated` を使用する(本末転倒)
|
||||
|
||||
## 使用場面
|
||||
|
||||
* iOS/macOSアプリのローカルデータストレージ(ユーザーデータ、設定、キャッシュコンテンツ)
|
||||
* 後でサーバーと同期するオフラインファーストアーキテクチャ
|
||||
* アプリの複数の部分から並行アクセスされる共有可変状態
|
||||
* `DispatchQueue` ベースのレガシーなスレッド安全機構を最新のSwift並行処理に置き換える
|
||||
217
docs/ja-JP/skills/swift-concurrency-6-2/SKILL.md
Normal file
217
docs/ja-JP/skills/swift-concurrency-6-2/SKILL.md
Normal file
@@ -0,0 +1,217 @@
|
||||
---
|
||||
name: swift-concurrency-6-2
|
||||
description: Swift 6.2のアクセシブルな並行処理——デフォルトはシングルスレッド、@concurrentは明示的なバックグラウンドオフロードに使用し、分離の一貫性はMainActor型に使用する。
|
||||
---
|
||||
|
||||
# Swift 6.2 アクセシブルな並行処理
|
||||
|
||||
コードがデフォルトでシングルスレッドで実行され、並行処理が明示的に導入されるSwift 6.2の並行処理モデルを採用したパターン。パフォーマンスを犠牲にすることなく、よくあるデータ競合エラーを排除する。
|
||||
|
||||
## 起動条件
|
||||
|
||||
* Swift 5.x または 6.0/6.1 プロジェクトを Swift 6.2 に移行する場合
|
||||
* データ競合安全性のコンパイラエラーを解決する場合
|
||||
* MainActorベースのアプリアーキテクチャを設計する場合
|
||||
* CPU集約的な処理をバックグラウンドスレッドにオフロードする場合
|
||||
* MainActor分離された型にプロトコル一貫性を実装する場合
|
||||
* Xcode 26で「アクセシブルな並行処理」ビルド設定を有効にする場合
|
||||
|
||||
## 核心的な問題:暗黙のバックグラウンドオフロード
|
||||
|
||||
Swift 6.1以前では、非同期関数が暗黙的にバックグラウンドスレッドにオフロードされ、一見安全に見えるコードでもデータ競合エラーを引き起こすことがあった:
|
||||
|
||||
```swift
|
||||
// Swift 6.1: ERROR
|
||||
@MainActor
|
||||
final class StickerModel {
|
||||
let photoProcessor = PhotoProcessor()
|
||||
|
||||
func extractSticker(_ item: PhotosPickerItem) async throws -> Sticker? {
|
||||
guard let data = try await item.loadTransferable(type: Data.self) else { return nil }
|
||||
|
||||
// Error: Sending 'self.photoProcessor' risks causing data races
|
||||
return await photoProcessor.extractSticker(data: data, with: item.itemIdentifier)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Swift 6.2ではこの問題が修正された:非同期関数はデフォルトで呼び出し元と同じActorに留まる。
|
||||
|
||||
```swift
|
||||
// Swift 6.2: OK — async stays on MainActor, no data race
|
||||
@MainActor
|
||||
final class StickerModel {
|
||||
let photoProcessor = PhotoProcessor()
|
||||
|
||||
func extractSticker(_ item: PhotosPickerItem) async throws -> Sticker? {
|
||||
guard let data = try await item.loadTransferable(type: Data.self) else { return nil }
|
||||
return await photoProcessor.extractSticker(data: data, with: item.itemIdentifier)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## コアパターン——分離の一貫性
|
||||
|
||||
MainActor型が非分離プロトコルに安全に準拠できるようになった:
|
||||
|
||||
```swift
|
||||
protocol Exportable {
|
||||
func export()
|
||||
}
|
||||
|
||||
// Swift 6.1: ERROR — crosses into main actor-isolated code
|
||||
// Swift 6.2: OK with isolated conformance
|
||||
extension StickerModel: @MainActor Exportable {
|
||||
func export() {
|
||||
photoProcessor.exportAsPNG()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
コンパイラはこの一貫性がMainActor上でのみ使用されることを保証する:
|
||||
|
||||
```swift
|
||||
// OK — ImageExporter is also @MainActor
|
||||
@MainActor
|
||||
struct ImageExporter {
|
||||
var items: [any Exportable]
|
||||
|
||||
mutating func add(_ item: StickerModel) {
|
||||
items.append(item) // Safe: same actor isolation
|
||||
}
|
||||
}
|
||||
|
||||
// ERROR — nonisolated context can't use MainActor conformance
|
||||
nonisolated struct ImageExporter {
|
||||
var items: [any Exportable]
|
||||
|
||||
mutating func add(_ item: StickerModel) {
|
||||
items.append(item) // Error: Main actor-isolated conformance cannot be used here
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## コアパターン——グローバル変数と静的変数
|
||||
|
||||
MainActorを使用してグローバル/静的状態を保護する:
|
||||
|
||||
```swift
|
||||
// Swift 6.1: ERROR — non-Sendable type may have shared mutable state
|
||||
final class StickerLibrary {
|
||||
static let shared: StickerLibrary = .init() // Error
|
||||
}
|
||||
|
||||
// Fix: Annotate with @MainActor
|
||||
@MainActor
|
||||
final class StickerLibrary {
|
||||
static let shared: StickerLibrary = .init() // OK
|
||||
}
|
||||
```
|
||||
|
||||
### MainActorデフォルト推論パターン
|
||||
|
||||
Swift 6.2ではMainActorをデフォルトで推論するパターンが導入された——手動の注釈なし:
|
||||
|
||||
```swift
|
||||
// With MainActor default inference enabled:
|
||||
final class StickerLibrary {
|
||||
static let shared: StickerLibrary = .init() // Implicitly @MainActor
|
||||
}
|
||||
|
||||
final class StickerModel {
|
||||
let photoProcessor: PhotoProcessor
|
||||
var selection: [PhotosPickerItem] // Implicitly @MainActor
|
||||
}
|
||||
|
||||
extension StickerModel: Exportable { // Implicitly @MainActor conformance
|
||||
func export() {
|
||||
photoProcessor.exportAsPNG()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
このパターンはオプトインで、アプリ、スクリプト、その他の実行可能ターゲットに推奨される。
|
||||
|
||||
## コアパターン——@concurrent を使ったバックグラウンド処理
|
||||
|
||||
真の並列処理が必要な場合、`@concurrent` を使って明示的にオフロードする:
|
||||
|
||||
> **重要:** この例は「アクセシブルな並行処理」ビルド設定——SE-0466 (MainActorデフォルト分離) と SE-0461 (デフォルト非分離非送信) の有効化が必要。これらの設定を有効にすると、`extractSticker` は呼び出し元のActorに留まり、可変状態へのアクセスが安全になる。**これらの設定なしでは、このコードにはデータ競合がある**——コンパイラがフラグを立てる。
|
||||
|
||||
```swift
|
||||
nonisolated final class PhotoProcessor {
|
||||
private var cachedStickers: [String: Sticker] = [:]
|
||||
|
||||
func extractSticker(data: Data, with id: String) async -> Sticker {
|
||||
if let sticker = cachedStickers[id] {
|
||||
return sticker
|
||||
}
|
||||
|
||||
let sticker = await Self.extractSubject(from: data)
|
||||
cachedStickers[id] = sticker
|
||||
return sticker
|
||||
}
|
||||
|
||||
// Offload expensive work to concurrent thread pool
|
||||
@concurrent
|
||||
static func extractSubject(from data: Data) async -> Sticker { /* ... */ }
|
||||
}
|
||||
|
||||
// Callers must await
|
||||
let processor = PhotoProcessor()
|
||||
processedPhotos[item.id] = await processor.extractSticker(data: data, with: item.id)
|
||||
```
|
||||
|
||||
`@concurrent` を使用するには:
|
||||
|
||||
1. コンテナとなる型に `nonisolated` をマークする
|
||||
2. 関数に `@concurrent` を追加する
|
||||
3. 関数がまだ非同期でない場合は `async` を追加する
|
||||
4. 呼び出し側に `await` を追加する
|
||||
|
||||
## 重要な設計上の決定
|
||||
|
||||
| 決定 | 理由 |
|
||||
|----------|-----------|
|
||||
| デフォルトシングルスレッド | 最も自然なコードはデータ競合がない。並行処理はオプトイン |
|
||||
| 非同期関数は呼び出し元のActorに留まる | データ競合エラーを引き起こす暗黙のオフロードを排除 |
|
||||
| 分離の一貫性 | MainActor型が安全でない回避策なしにプロトコルに準拠できる |
|
||||
| `@concurrent` による明示的なオプトイン | バックグラウンド実行は偶発的なものではなく意図的なパフォーマンス選択 |
|
||||
| MainActorデフォルト推論 | アプリターゲットの定型的な `@MainActor` 注釈を削減 |
|
||||
| オプトイン採用 | 非破壊的な移行パス——機能を段階的に有効化 |
|
||||
|
||||
## 移行手順
|
||||
|
||||
1. **Xcodeで有効化**:ビルド設定のSwift Compiler > Concurrencyセクション
|
||||
2. **SPMで有効化**:パッケージマニフェストで `SwiftSettings` APIを使用
|
||||
3. **移行ツールを使用**:swift.org/migrationを通じて自動コード変更
|
||||
4. **MainActorデフォルトから始める**:アプリターゲットの推論モードを有効化
|
||||
5. **必要な場所に `@concurrent` を追加**:まずプロファイリングし、ホットパスをオフロード
|
||||
6. **徹底的にテスト**:データ競合の問題はコンパイル時エラーになる
|
||||
|
||||
## ベストプラクティス
|
||||
|
||||
* **MainActorから始める** —— まずシングルスレッドコードを書き、後で最適化する
|
||||
* **CPU集約的な処理のみに `@concurrent` を使用する** —— 画像処理、圧縮、複雑な計算
|
||||
* **主にシングルスレッドのアプリターゲットのMainActor推論モードを有効にする**
|
||||
* **オフロード前にプロファイリングする** —— Instrumentsで実際のボトルネックを見つける
|
||||
* **グローバル変数を保護するために MainActor を使用する** —— グローバル/静的な可変状態にはActor分離が必要
|
||||
* **`nonisolated` 回避策や `@Sendable` ラッパーではなく分離の一貫性を使用する**
|
||||
* **段階的に移行する** —— ビルド設定で一度に1つの機能を有効化する
|
||||
|
||||
## 避けるべきアンチパターン
|
||||
|
||||
* すべての非同期関数に `@concurrent` を適用する(ほとんどはバックグラウンド実行を必要としない)
|
||||
* 分離を理解せずにコンパイラエラーを抑制するために `nonisolated` を使用する
|
||||
* Actorが同じ安全性を提供できる場面でレガシーの `DispatchQueue` パターンを保持する
|
||||
* 並行処理関連のFoundation Modelsコードで `model.availability` チェックをスキップする
|
||||
* コンパイラと戦う——データ競合をレポートしている場合、コードには本当の並行処理の問題がある
|
||||
* すべての非同期コードがバックグラウンドで実行されると仮定する(Swift 6.2のデフォルト:呼び出し元のActorに留まる)
|
||||
|
||||
## 使用場面
|
||||
|
||||
* すべての新しいSwift 6.2+プロジェクト(「アクセシブルな並行処理」は推奨されるデフォルト設定)
|
||||
* Swift 5.x または 6.0/6.1 の並行処理から既存のアプリを移行する場合
|
||||
* Xcode 26の採用中にデータ競合安全性のコンパイラエラーを解決する場合
|
||||
* MainActorを中心としたアプリアーキテクチャを構築する場合(ほとんどのUIアプリ)
|
||||
* パフォーマンス最適化——特定の重い計算をバックグラウンドにオフロードする場合
|
||||
190
docs/ja-JP/skills/swift-protocol-di-testing/SKILL.md
Normal file
190
docs/ja-JP/skills/swift-protocol-di-testing/SKILL.md
Normal file
@@ -0,0 +1,190 @@
|
||||
---
|
||||
name: swift-protocol-di-testing
|
||||
description: テスト可能なSwiftコードのためのプロトコルベースの依存性注入——焦点を絞ったプロトコルとSwift Testingを使用してファイルシステム、ネットワーク、外部APIをモックする。
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# プロトコルベースのSwift依存性注入テスト
|
||||
|
||||
外部の依存関係(ファイルシステム、ネットワーク、iCloud)を小さく焦点を絞ったプロトコルとして抽象化することで、SwiftコードをテストしやすくするパターンI/Oなしの決定論的テストをサポートする。
|
||||
|
||||
## 起動条件
|
||||
|
||||
* ファイルシステム、ネットワーク、または外部APIにアクセスするSwiftコードを書く場合
|
||||
* 実際の障害を起こさずにエラー処理パスをテストする必要がある場合
|
||||
* 異なる環境(アプリ、テスト、SwiftUIプレビュー)で動作するモジュールを構築する場合
|
||||
* Swift並行処理(Actor、Sendable)をサポートするテスト可能なアーキテクチャを設計する場合
|
||||
|
||||
## コアパターン
|
||||
|
||||
### 1. 小さく焦点を絞ったプロトコルを定義する
|
||||
|
||||
各プロトコルは1つの外部関心事のみを処理する。
|
||||
|
||||
```swift
|
||||
// File system access
|
||||
public protocol FileSystemProviding: Sendable {
|
||||
func containerURL(for purpose: Purpose) -> URL?
|
||||
}
|
||||
|
||||
// File read/write operations
|
||||
public protocol FileAccessorProviding: Sendable {
|
||||
func read(from url: URL) throws -> Data
|
||||
func write(_ data: Data, to url: URL) throws
|
||||
func fileExists(at url: URL) -> Bool
|
||||
}
|
||||
|
||||
// Bookmark storage (e.g., for sandboxed apps)
|
||||
public protocol BookmarkStorageProviding: Sendable {
|
||||
func saveBookmark(_ data: Data, for key: String) throws
|
||||
func loadBookmark(for key: String) throws -> Data?
|
||||
}
|
||||
```
|
||||
|
||||
### 2. デフォルト(本番用)実装を作成する
|
||||
|
||||
```swift
|
||||
public struct DefaultFileSystemProvider: FileSystemProviding {
|
||||
public init() {}
|
||||
|
||||
public func containerURL(for purpose: Purpose) -> URL? {
|
||||
FileManager.default.url(forUbiquityContainerIdentifier: nil)
|
||||
}
|
||||
}
|
||||
|
||||
public struct DefaultFileAccessor: FileAccessorProviding {
|
||||
public init() {}
|
||||
|
||||
public func read(from url: URL) throws -> Data {
|
||||
try Data(contentsOf: url)
|
||||
}
|
||||
|
||||
public func write(_ data: Data, to url: URL) throws {
|
||||
try data.write(to: url, options: .atomic)
|
||||
}
|
||||
|
||||
public func fileExists(at url: URL) -> Bool {
|
||||
FileManager.default.fileExists(atPath: url.path)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. テスト用のモック実装を作成する
|
||||
|
||||
```swift
|
||||
public final class MockFileAccessor: FileAccessorProviding, @unchecked Sendable {
|
||||
public var files: [URL: Data] = [:]
|
||||
public var readError: Error?
|
||||
public var writeError: Error?
|
||||
|
||||
public init() {}
|
||||
|
||||
public func read(from url: URL) throws -> Data {
|
||||
if let error = readError { throw error }
|
||||
guard let data = files[url] else {
|
||||
throw CocoaError(.fileReadNoSuchFile)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
public func write(_ data: Data, to url: URL) throws {
|
||||
if let error = writeError { throw error }
|
||||
files[url] = data
|
||||
}
|
||||
|
||||
public func fileExists(at url: URL) -> Bool {
|
||||
files[url] != nil
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. デフォルトパラメーターで依存関係を注入する
|
||||
|
||||
本番コードはデフォルト値を使用し、テストはモックを注入する。
|
||||
|
||||
```swift
|
||||
public actor SyncManager {
|
||||
private let fileSystem: FileSystemProviding
|
||||
private let fileAccessor: FileAccessorProviding
|
||||
|
||||
public init(
|
||||
fileSystem: FileSystemProviding = DefaultFileSystemProvider(),
|
||||
fileAccessor: FileAccessorProviding = DefaultFileAccessor()
|
||||
) {
|
||||
self.fileSystem = fileSystem
|
||||
self.fileAccessor = fileAccessor
|
||||
}
|
||||
|
||||
public func sync() async throws {
|
||||
guard let containerURL = fileSystem.containerURL(for: .sync) else {
|
||||
throw SyncError.containerNotAvailable
|
||||
}
|
||||
let data = try fileAccessor.read(
|
||||
from: containerURL.appendingPathComponent("data.json")
|
||||
)
|
||||
// Process data...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Swift Testingを使用してテストを書く
|
||||
|
||||
```swift
|
||||
import Testing
|
||||
|
||||
@Test("Sync manager handles missing container")
|
||||
func testMissingContainer() async {
|
||||
let mockFileSystem = MockFileSystemProvider(containerURL: nil)
|
||||
let manager = SyncManager(fileSystem: mockFileSystem)
|
||||
|
||||
await #expect(throws: SyncError.containerNotAvailable) {
|
||||
try await manager.sync()
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Sync manager reads data correctly")
|
||||
func testReadData() async throws {
|
||||
let mockFileAccessor = MockFileAccessor()
|
||||
mockFileAccessor.files[testURL] = testData
|
||||
|
||||
let manager = SyncManager(fileAccessor: mockFileAccessor)
|
||||
let result = try await manager.loadData()
|
||||
|
||||
#expect(result == expectedData)
|
||||
}
|
||||
|
||||
@Test("Sync manager handles read errors gracefully")
|
||||
func testReadError() async {
|
||||
let mockFileAccessor = MockFileAccessor()
|
||||
mockFileAccessor.readError = CocoaError(.fileReadCorruptFile)
|
||||
|
||||
let manager = SyncManager(fileAccessor: mockFileAccessor)
|
||||
|
||||
await #expect(throws: SyncError.self) {
|
||||
try await manager.sync()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## ベストプラクティス
|
||||
|
||||
* **単一責任**:各プロトコルは1つの関心事を処理する——多くのメソッドを持つ「ゴッドプロトコル」を作らない
|
||||
* **Sendable 一貫性**:プロトコルがActor境界をまたいで使用される場合に必要
|
||||
* **デフォルトパラメーター**:本番コードは実際の実装をデフォルトで使用する。テストだけがモックを指定する必要がある
|
||||
* **エラーのモック**:障害パスをテストするために設定可能なエラープロパティを持つモックを設計する
|
||||
* **境界のみをモック**:外部の依存関係(ファイルシステム、ネットワーク、API)をモックし、内部型はモックしない
|
||||
|
||||
## 避けるべきアンチパターン
|
||||
|
||||
* すべての外部アクセスをカバーする単一の大きなプロトコルを作成する
|
||||
* 外部の依存関係を持たない内部型をモックする
|
||||
* 適切な依存性注入の代わりに `#if DEBUG` 条件文を使用する
|
||||
* Actorと組み合わせて使用する際に `Sendable` 一貫性を忘れる
|
||||
* 過度な設計:型が外部の依存関係を持たない場合、プロトコルは必要ない
|
||||
|
||||
## 使用場面
|
||||
|
||||
* ファイルシステム、ネットワーク、または外部APIに触れるあらゆるSwiftコード
|
||||
* 実際の環境では引き起こすことが難しいエラー処理パスをテストする場合
|
||||
* アプリ、テスト、SwiftUIプレビューのコンテキストで動作するモジュールを構築する場合
|
||||
* Swift並行処理(Actor、構造化並行処理)を採用したテスト可能なアーキテクチャが必要なアプリ
|
||||
259
docs/ja-JP/skills/swiftui-patterns/SKILL.md
Normal file
259
docs/ja-JP/skills/swiftui-patterns/SKILL.md
Normal file
@@ -0,0 +1,259 @@
|
||||
---
|
||||
name: swiftui-patterns
|
||||
description: @Observableを使用した状態管理、ビュー合成、ナビゲーション、パフォーマンス最適化、モダンなiOS/macOS UIのベストプラクティスを備えたSwiftUIアーキテクチャパターン。
|
||||
---
|
||||
|
||||
# SwiftUI パターン
|
||||
|
||||
Appleプラットフォーム向けのモダンなSwiftUIパターン。宣言的で高性能なユーザーインターフェースを構築するために使用する。Observationフレームワーク、ビュー合成、型安全なナビゲーション、パフォーマンス最適化をカバーする。
|
||||
|
||||
## 起動条件
|
||||
|
||||
* SwiftUIビューを構築し、状態を管理する場合(`@State`、`@Observable`、`@Binding`)
|
||||
* `NavigationStack` を使用したナビゲーションフローを設計する場合
|
||||
* ビューモデルとデータフローを構築する場合
|
||||
* リストと複雑なレイアウトのレンダリングパフォーマンスを最適化する場合
|
||||
* SwiftUIで環境値と依存性注入を使用する場合
|
||||
|
||||
## 状態管理
|
||||
|
||||
### プロパティラッパーの選択
|
||||
|
||||
最も適したシンプルなラッパーを選択する:
|
||||
|
||||
| ラッパー | 使用場面 |
|
||||
|---------|----------|
|
||||
| `@State` | ビューローカルな値型(トグル、フォームフィールド、シート表示) |
|
||||
| `@Binding` | 親ビューの `@State` への双方向参照 |
|
||||
| `@Observable` クラス + `@State` | 複数のプロパティを持つ所有モデル |
|
||||
| `@Observable` クラス(ラッパーなし) | 親ビューから渡される読み取り専用参照 |
|
||||
| `@Bindable` | `@Observable` プロパティへの双方向バインディング |
|
||||
| `@Environment` | `.environment()` で注入された共有依存関係 |
|
||||
|
||||
### @Observable ViewModel
|
||||
|
||||
`ObservableObject` ではなく `@Observable` を使用する——プロパティレベルの変更を追跡するため、SwiftUIは変更されたプロパティを読み取ったビューのみを再レンダリングする:
|
||||
|
||||
```swift
|
||||
@Observable
|
||||
final class ItemListViewModel {
|
||||
private(set) var items: [Item] = []
|
||||
private(set) var isLoading = false
|
||||
var searchText = ""
|
||||
|
||||
private let repository: any ItemRepository
|
||||
|
||||
init(repository: any ItemRepository = DefaultItemRepository()) {
|
||||
self.repository = repository
|
||||
}
|
||||
|
||||
func load() async {
|
||||
isLoading = true
|
||||
defer { isLoading = false }
|
||||
items = (try? await repository.fetchAll()) ?? []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### ViewModelを使用するビュー
|
||||
|
||||
```swift
|
||||
struct ItemListView: View {
|
||||
@State private var viewModel: ItemListViewModel
|
||||
|
||||
init(viewModel: ItemListViewModel = ItemListViewModel()) {
|
||||
_viewModel = State(initialValue: viewModel)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
List(viewModel.items) { item in
|
||||
ItemRow(item: item)
|
||||
}
|
||||
.searchable(text: $viewModel.searchText)
|
||||
.overlay { if viewModel.isLoading { ProgressView() } }
|
||||
.task { await viewModel.load() }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 環境への注入
|
||||
|
||||
`@EnvironmentObject` の代わりに `@Environment` を使用する:
|
||||
|
||||
```swift
|
||||
// Inject
|
||||
ContentView()
|
||||
.environment(authManager)
|
||||
|
||||
// Consume
|
||||
struct ProfileView: View {
|
||||
@Environment(AuthManager.self) private var auth
|
||||
|
||||
var body: some View {
|
||||
Text(auth.currentUser?.name ?? "Guest")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## ビュー合成
|
||||
|
||||
### 無効化を制限するためにサブビューを抽出する
|
||||
|
||||
ビューを小さく焦点を絞った構造体に分割する。状態が変化した場合、その状態を読み取ったサブビューのみが再レンダリングされる:
|
||||
|
||||
```swift
|
||||
struct OrderView: View {
|
||||
@State private var viewModel = OrderViewModel()
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
OrderHeader(title: viewModel.title)
|
||||
OrderItemList(items: viewModel.items)
|
||||
OrderTotal(total: viewModel.total)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 再利用可能なスタイルのための ViewModifier
|
||||
|
||||
```swift
|
||||
struct CardModifier: ViewModifier {
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.padding()
|
||||
.background(.regularMaterial)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
func cardStyle() -> some View {
|
||||
modifier(CardModifier())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## ナビゲーション
|
||||
|
||||
### 型安全な NavigationStack
|
||||
|
||||
`NavigationStack` と `NavigationPath` を使用して、プログラム的で型安全なルーティングを実現する:
|
||||
|
||||
```swift
|
||||
@Observable
|
||||
final class Router {
|
||||
var path = NavigationPath()
|
||||
|
||||
func navigate(to destination: Destination) {
|
||||
path.append(destination)
|
||||
}
|
||||
|
||||
func popToRoot() {
|
||||
path = NavigationPath()
|
||||
}
|
||||
}
|
||||
|
||||
enum Destination: Hashable {
|
||||
case detail(Item.ID)
|
||||
case settings
|
||||
case profile(User.ID)
|
||||
}
|
||||
|
||||
struct RootView: View {
|
||||
@State private var router = Router()
|
||||
|
||||
var body: some View {
|
||||
NavigationStack(path: $router.path) {
|
||||
HomeView()
|
||||
.navigationDestination(for: Destination.self) { dest in
|
||||
switch dest {
|
||||
case .detail(let id): ItemDetailView(itemID: id)
|
||||
case .settings: SettingsView()
|
||||
case .profile(let id): ProfileView(userID: id)
|
||||
}
|
||||
}
|
||||
}
|
||||
.environment(router)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## パフォーマンス
|
||||
|
||||
### 大規模なコレクションにレイジーコンテナを使用する
|
||||
|
||||
`LazyVStack` と `LazyHStack` はビューが表示される時のみ作成する:
|
||||
|
||||
```swift
|
||||
ScrollView {
|
||||
LazyVStack(spacing: 8) {
|
||||
ForEach(items) { item in
|
||||
ItemRow(item: item)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 安定した識別子
|
||||
|
||||
`ForEach` では常に安定した一意のIDを使用する——配列インデックスは避ける:
|
||||
|
||||
```swift
|
||||
// Use Identifiable conformance or explicit id
|
||||
ForEach(items, id: \.stableID) { item in
|
||||
ItemRow(item: item)
|
||||
}
|
||||
```
|
||||
|
||||
### body 内での高コストな操作を避ける
|
||||
|
||||
* `body` 内でI/O、ネットワーク呼び出し、重い計算を絶対に実行しない
|
||||
* 非同期処理には `.task {}` を使用する——ビューが消えると自動的にキャンセルされる
|
||||
* スクロールビューでは `.sensoryFeedback()` と `.geometryGroup()` を慎重に使用する
|
||||
* リストでは `.shadow()`、`.blur()`、`.mask()` の使用を最小化する——画面外レンダリングを引き起こす
|
||||
|
||||
### Equatable に準拠する
|
||||
|
||||
bodyの計算が高コストなビューには、不要な再レンダリングをスキップするために `Equatable` に準拠する:
|
||||
|
||||
```swift
|
||||
struct ExpensiveChartView: View, Equatable {
|
||||
let dataPoints: [DataPoint] // DataPoint must conform to Equatable
|
||||
|
||||
static func == (lhs: Self, rhs: Self) -> Bool {
|
||||
lhs.dataPoints == rhs.dataPoints
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
// Complex chart rendering
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## プレビュー
|
||||
|
||||
インラインのモックデータで `#Preview` マクロを使用して素早い反復を行う:
|
||||
|
||||
```swift
|
||||
#Preview("Empty state") {
|
||||
ItemListView(viewModel: ItemListViewModel(repository: EmptyMockRepository()))
|
||||
}
|
||||
|
||||
#Preview("Loaded") {
|
||||
ItemListView(viewModel: ItemListViewModel(repository: PopulatedMockRepository()))
|
||||
}
|
||||
```
|
||||
|
||||
## 避けるべきアンチパターン
|
||||
|
||||
* 新しいコードで `ObservableObject` / `@Published` / `@StateObject` / `@EnvironmentObject` を使用する——`@Observable` に移行する
|
||||
* `body` や `init` 内に直接非同期処理を置く——`.task {}` または明示的なロードメソッドを使用する
|
||||
* データを所有しないサブビューでViewModelを `@State` として作成する——代わりに親ビューから渡す
|
||||
* `AnyView` による型消去を使用する——条件付きビューには `@ViewBuilder` または `Group` を優先する
|
||||
* ActorとのデータのやりとりにおいてSendable要件を無視する
|
||||
|
||||
## 参照
|
||||
|
||||
Actorベースの永続化パターンについては、スキル `swift-actor-persistence` を参照。
|
||||
プロトコルベースのDIとSwift Testingを使用したテストについては、スキル `swift-protocol-di-testing` を参照。
|
||||
165
docs/ja-JP/skills/team-builder/SKILL.md
Normal file
165
docs/ja-JP/skills/team-builder/SKILL.md
Normal file
@@ -0,0 +1,165 @@
|
||||
---
|
||||
name: team-builder
|
||||
description: 並列チームを構成して派遣するためのインタラクティブなエージェント選択ツール
|
||||
origin: community
|
||||
---
|
||||
|
||||
# チームビルダー
|
||||
|
||||
オンデマンドでエージェントチームを閲覧・構成するためのインタラクティブメニュー。フラット構成またはドメインサブディレクトリで整理されたエージェントコレクションに対応する。
|
||||
|
||||
## 使用場面
|
||||
|
||||
* 複数のエージェントロール(markdownファイル)があり、あるタスクにどのエージェントを使うか選択したい場合
|
||||
* 異なるドメインから(例えば、セキュリティ + SEO + アーキテクチャ)臨時チームを結成したい場合
|
||||
* 決定する前に利用可能なエージェントを閲覧したい場合
|
||||
|
||||
## 前提条件
|
||||
|
||||
エージェントファイルはロール、プロンプト(アイデンティティ、ルール、ワークフロー、成果物)を含むmarkdownファイルである必要がある。最初の `# Heading` がエージェント名として使用され、最初の段落が説明として使用される。
|
||||
|
||||
フラット構成とサブディレクトリの両方のレイアウトをサポートする:
|
||||
|
||||
**サブディレクトリレイアウト** —— ドメインはフォルダー名から推論される:
|
||||
|
||||
```
|
||||
agents/
|
||||
├── engineering/
|
||||
│ ├── security-engineer.md
|
||||
│ └── software-architect.md
|
||||
├── marketing/
|
||||
│ └── seo-specialist.md
|
||||
└── sales/
|
||||
└── discovery-coach.md
|
||||
```
|
||||
|
||||
**フラットレイアウト** —— ドメインは共有のファイル名プレフィックスから推論される。2つ以上のファイルが同じプレフィックスを共有する場合、そのプレフィックスはドメインとみなされる。ユニークなプレフィックスを持つファイルは「General」カテゴリーに分類される。注意:アルゴリズムは最初の `-` で分割するため、複数単語のドメイン(例:`product-management`)にはサブディレクトリレイアウトを使用する:
|
||||
|
||||
```
|
||||
agents/
|
||||
├── engineering-security-engineer.md
|
||||
├── engineering-software-architect.md
|
||||
├── marketing-seo-specialist.md
|
||||
├── marketing-content-strategist.md
|
||||
├── sales-discovery-coach.md
|
||||
└── sales-outbound-strategist.md
|
||||
```
|
||||
|
||||
## 設定
|
||||
|
||||
エージェントディレクトリは順番に探索され、結果がマージされる:
|
||||
|
||||
1. `./agents/**/*.md` + `./agents/*.md` —— プロジェクトローカルのエージェント(両方の深さ)
|
||||
2. `~/.claude/agents/**/*.md` + `~/.claude/agents/*.md` —— グローバルエージェント(両方の深さ)
|
||||
|
||||
すべての場所の結果がマージされ、エージェント名で重複排除される。同名の場合、プロジェクトローカルのエージェントがグローバルエージェントより優先される。ユーザーがカスタムパスを指定した場合、そのパスを代わりに使用する。
|
||||
|
||||
## 動作原理
|
||||
|
||||
### ステップ 1:利用可能なエージェントを発見する
|
||||
|
||||
上記の探索順序を使用してエージェントディレクトリでグローバル検索を実行する。READMEファイルを除外する。見つかった各ファイルに対して:
|
||||
|
||||
* **サブディレクトリレイアウト:** 親フォルダー名からドメインを抽出する
|
||||
* **フラットレイアウト:** すべてのファイル名プレフィックス(最初の `-` より前のテキスト)を収集する。プレフィックスが2つ以上のファイル名に現れる場合のみドメインとして適格(例:`engineering-security-engineer.md` と `engineering-software-architect.md` はどちらも `engineering` で始まる → Engineeringドメイン)。ユニークなプレフィックスを持つファイル(例:`code-reviewer.md`、`tdd-guide.md`)は「General」カテゴリーに分類される
|
||||
* 最初の `# Heading` からエージェント名を抽出する。見出しが見つからない場合は、ファイル名から名前を導出する(`.md` を除去し、ハイフンをスペースに置換し、タイトルケースに変換)
|
||||
* 見出しの後の最初の段落から一行のサマリーを抽出する
|
||||
|
||||
すべての場所を探索した後にエージェントファイルが見つからない場合、ユーザーに通知する:「エージェントファイルが見つかりませんでした。確認済み:\[探索済みパスのリスト]。期待されるもの:これらのディレクトリ内のmarkdownファイル。」そして停止する。
|
||||
|
||||
### ステップ 2:ドメインメニューを表示する
|
||||
|
||||
```
|
||||
利用可能なエージェントドメイン:
|
||||
1. エンジニアリング — ソフトウェアアーキテクト、セキュリティエンジニア
|
||||
2. マーケティング — SEOスペシャリスト
|
||||
3. セールス — ディスカバリーコーチ、アウトバウンドストラテジスト
|
||||
|
||||
ドメインを選択するか、特定のエージェントを指定してください(例:「1,3」または「security + seo」):
|
||||
```
|
||||
|
||||
* エージェント数がゼロのドメインはスキップする(空ディレクトリ)
|
||||
* 各ドメインのエージェント数を表示する
|
||||
|
||||
### ステップ 3:選択を処理する
|
||||
|
||||
柔軟な入力を受け付ける:
|
||||
|
||||
* 数字:「1,3」でEngineeringとSalesのすべてのエージェントを選択
|
||||
* 名前:「security + seo」で発見されたエージェントに対してファジーマッチング
|
||||
* 「all from engineering」でそのドメインのすべてのエージェントを選択
|
||||
|
||||
5つ以上のエージェントが選択された場合、アルファベット順にリストアップして絞り込みを求める:「Nつのエージェントを選択しました(最大5つ)。どれを保持するか選択するか、アルファベット順の最初の5つを使用する場合は 'first 5' と言ってください。」
|
||||
|
||||
選択を確認する:
|
||||
|
||||
```
|
||||
選択済み:セキュリティエンジニア + SEOスペシャリスト
|
||||
どのようなタスクに取り組む予定ですか?(タスクを説明してください)
|
||||
```
|
||||
|
||||
### ステップ 4:エージェントを並列で起動する
|
||||
|
||||
1. 選択された各エージェントのmarkdownファイルを読み取る
|
||||
2. まだ提供されていない場合は、タスクの説明を求める
|
||||
3. Agentツールを使用してすべてのエージェントを並列で起動する:
|
||||
* `subagent_type: "general-purpose"`
|
||||
* `prompt: "{agent file content}\n\nTask: {task description}"`
|
||||
* 各エージェントは独立して実行する——エージェント間の通信は不要
|
||||
4. エージェントが失敗した場合(エラー、タイムアウト、または空の出力)、インラインで失敗を記録し(例:「Security Engineer: failed — \[理由]」)、成功したエージェントの結果の処理を続ける
|
||||
|
||||
### ステップ 5:結果を統合する
|
||||
|
||||
すべての出力を収集して統一されたレポートを提示する:
|
||||
|
||||
* エージェント別にグループ化された結果
|
||||
* 統合セクションで強調:
|
||||
* エージェント間のコンセンサス
|
||||
* 提案間の衝突または矛盾
|
||||
* 推奨される次のステップ
|
||||
|
||||
1つのエージェントのみが選択された場合は、統合セクションをスキップして直接出力を提示する。
|
||||
|
||||
## ルール
|
||||
|
||||
* **動的発見のみ。** エージェントリストをハードコードしない。ディレクトリの新しいファイルはメニューに自動的に表示される。
|
||||
* **チームあたり最大5つのエージェント。** 5つを超えると収益逓減とトークン使用量が多くなる。選択時に強制する。
|
||||
* **並列分散。** すべてのエージェントが同時に実行される——Agentツールの並列呼び出しパターンを使用する。
|
||||
* **並列Agent呼び出し(TeamCreateではない)。** このスキルは独立した作業の処理に並列Agentツール呼び出しを使用する。エージェントが議論したり互いに回答する必要がある場合のみ、TeamCreate(マルチエージェント会話のためのClaude Codeツール)が必要になる。
|
||||
|
||||
## 例
|
||||
|
||||
```
|
||||
ユーザー: team builder
|
||||
|
||||
Claude:
|
||||
利用可能なエージェントドメイン:
|
||||
1. エンジニアリング (2) — ソフトウェアアーキテクト、セキュリティエンジニア
|
||||
2. マーケティング (1) — SEOスペシャリスト
|
||||
3. セールス (4) — ディスカバリーコーチ、アウトバウンドストラテジスト、プロポーザルストラテジスト、セールスエンジニア
|
||||
4. サポート (1) — エグゼクティブサマリー
|
||||
|
||||
ドメインを選択するか、特定のエージェントを指定してください:
|
||||
|
||||
ユーザー: security + seo
|
||||
|
||||
Claude:
|
||||
選択済み:セキュリティエンジニア + SEOスペシャリスト
|
||||
どのようなタスクに取り組む予定ですか?
|
||||
|
||||
ユーザー: リリース前に私のNext.jsのeコマースサイトをレビューしてほしい
|
||||
|
||||
[2つのエージェントが並列で起動され、それぞれがコードベースに専門知識を適用する]
|
||||
|
||||
Claude:
|
||||
## セキュリティエンジニアの発見事項
|
||||
- [発見内容...]
|
||||
|
||||
## SEOスペシャリストの発見事項
|
||||
- [発見内容...]
|
||||
|
||||
## 統合分析
|
||||
両エージェントが同意:[...]
|
||||
衝突点:セキュリティが推奨するCSPがインラインスタイルをブロックし、SEOはインラインのschemaマークアップを必要とする。解決策:[...]
|
||||
次のステップ:[...]
|
||||
```
|
||||
109
docs/ja-JP/skills/terminal-ops/SKILL.md
Normal file
109
docs/ja-JP/skills/terminal-ops/SKILL.md
Normal file
@@ -0,0 +1,109 @@
|
||||
---
|
||||
name: terminal-ops
|
||||
description: ECCのための証拠優先のリポジトリ実行ワークフロー。ユーザーがコマンドの実行、リポジトリの確認、CIの失敗のデバッグ、正確な実行と検証の証明を伴う狭い修正のプッシュを必要とする場合に使用する。
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# ターミナルオペレーション
|
||||
|
||||
ユーザーが実際のリポジトリ実行を必要とする場合にこのスキルを使用する:コマンドの実行、git状態の確認、CIまたはビルドのデバッグ、狭い修正の実施、変更と検証内容の正確なレポート。
|
||||
|
||||
このスキルは意図的に汎用的なコーディングガイダンスよりも範囲が狭い。これは証拠優先のターミナル実行操作ワークフローである。
|
||||
|
||||
## スキルスタック
|
||||
|
||||
関連する場合、これらのECCネイティブスキルをワークフローに組み込む:
|
||||
|
||||
* `verification-loop` は変更後の正確な検証ステップに使用
|
||||
* `tdd-workflow` は正しい修正に回帰カバレッジが必要な場合に使用
|
||||
* `security-review` はキー、認証、外部入力が絡む場合に使用
|
||||
* `github-ops` はタスクがCI実行、PRステータス、またはリリース状態に依存する場合に使用
|
||||
* `knowledge-ops` は検証結果を永続的なプロジェクトコンテキストに保存する必要がある場合に使用
|
||||
|
||||
## 使用場面
|
||||
|
||||
* ユーザーが「修正」「デバッグ」「これを実行」「リポジトリを確認」「プッシュ」と言う場合
|
||||
* タスクがコマンド出力、git状態、テスト結果、または検証済みのローカル修正に依存する場合
|
||||
* 答えが以下を区別する必要がある場合:ローカルで変更済み、ローカルで検証済み、コミット済み、プッシュ済み
|
||||
|
||||
## 安全策
|
||||
|
||||
* 編集前に確認する
|
||||
* ユーザーが監査/レビューのみを要求している場合は読み取り専用を維持する
|
||||
* アドホックなラッパーではなく、リポジトリローカルのスクリプトとヘルパーを優先する
|
||||
* 検証コマンドが再実行されるまで、修正済みと主張しない
|
||||
* ブランチが実際に上流にプッシュされるまで、プッシュ済みと主張しない
|
||||
|
||||
## ワークフロー
|
||||
|
||||
### 1. 作業サーフェスを特定する
|
||||
|
||||
以下を明確にする:
|
||||
|
||||
* 正確なリポジトリパス
|
||||
* ブランチ
|
||||
* ローカル差分の状態
|
||||
* 要求されたモード:
|
||||
* 確認
|
||||
* 修正
|
||||
* 検証
|
||||
* プッシュ
|
||||
|
||||
### 2. まず失敗サーフェスを読み取る
|
||||
|
||||
何かを変更する前に:
|
||||
|
||||
* エラーを確認する
|
||||
* ファイルまたはテストを確認する
|
||||
* git状態を確認する
|
||||
* 盲目的に再読み込みする前に、提供されたログまたはコンテキストを使用する
|
||||
|
||||
### 3. 修正を狭い範囲に保つ
|
||||
|
||||
一度に1つの主な失敗に対処する:
|
||||
|
||||
* 最初に最小限の有用な検証コマンドを使用する
|
||||
* ローカルの失敗が解決した後のみ、より大きなビルド/テストプロセスにエスカレートする
|
||||
* コマンドが同じ特性で失敗し続ける場合、広範囲なリトライを停止して絞り込む
|
||||
|
||||
### 4. 正確な実行状態をレポートする
|
||||
|
||||
正確な状態語を使用する:
|
||||
|
||||
* 確認済み
|
||||
* ローカルで変更済み
|
||||
* ローカルで検証済み
|
||||
* コミット済み
|
||||
* プッシュ済み
|
||||
* ブロック済み
|
||||
|
||||
## 出力フォーマット
|
||||
|
||||
```text
|
||||
サーフェス
|
||||
- リポジトリ
|
||||
- ブランチ
|
||||
- 要求されたモード
|
||||
|
||||
証拠
|
||||
- 失敗したコマンド / 差分 / テスト
|
||||
|
||||
アクション
|
||||
- 変更した内容
|
||||
|
||||
状態
|
||||
- 確認済み / ローカルで変更済み / ローカルで検証済み / コミット済み / プッシュ済み / ブロック済み
|
||||
```
|
||||
|
||||
## 落とし穴
|
||||
|
||||
* ライブなリポジトリ状態を読み取れる場合に古い記憶に頼らない
|
||||
* 狭い修正をリポジトリ全体の変更に拡大しない
|
||||
* 破壊的なgitコマンドを使用しない
|
||||
* 関連のないローカル作業を無視しない
|
||||
|
||||
## 検証
|
||||
|
||||
* レスポンスには検証コマンドまたはテストを示す
|
||||
* gitに関する作業にはリポジトリパスとブランチを示す
|
||||
* プッシュの主張には対象ブランチと正確な結果を含める
|
||||
121
docs/ja-JP/skills/token-budget-advisor/SKILL.md
Normal file
121
docs/ja-JP/skills/token-budget-advisor/SKILL.md
Normal file
@@ -0,0 +1,121 @@
|
||||
---
|
||||
name: token-budget-advisor
|
||||
description: 回答する前に、どれだけの回答深度を消費するかについてユーザーに情報に基づいた選択を提供する。ユーザーが回答の長さ、深さ、またはトークンバジェットを明示的に制御したい場合にこのスキルを使用する。トリガー条件:"token budget", "token count", "token usage", "token limit", "response length", "answer depth", "short version", "brief answer", "detailed answer", "exhaustive answer", "respuesta corta vs larga", "cuántos tokens", "ahorrar tokens", "responde al 50%", "dame la versión corta", "quiero controlar cuánto usas"、またはユーザーが回答のサイズや深さの制御を明示的に求めるその他の明確なバリエーション。トリガーしない条件:ユーザーが現在のセッションでレベルを指定済み(そのレベルを維持)、リクエストが明らかに一言の回答、または「token」が認証/セッション/支払いトークンを指している。origin: community
|
||||
---
|
||||
|
||||
# トークンバジェットアドバイザー(TBA)
|
||||
|
||||
Claudeが回答する前にレスポンスフローをインターセプトし、ユーザーが回答の深さを選択できるようにする。
|
||||
|
||||
## 使用場面
|
||||
|
||||
* ユーザーが回答の長さや詳細度を制御したい場合
|
||||
* ユーザーがトークン、バジェット、深さ、または回答の長さに言及する場合
|
||||
* ユーザーが「短いバージョン」「TL;DR」「簡潔に」「25%」「詳細に」などと言う場合
|
||||
* ユーザーが事前に深さ/詳細度を選択したい場合
|
||||
|
||||
**トリガーしない場合**:ユーザーが本セッションですでにレベルを設定している(静かに維持)、または回答が本質的に一行。
|
||||
|
||||
## 動作原理
|
||||
|
||||
### ステップ 1 — 入力トークンを推定する
|
||||
|
||||
リポジトリの標準コンテキストバジェットのヒューリスティックスを使用して、プロンプトのトークン数を頭の中で推定する。
|
||||
|
||||
[context-budget](../context-budget/SKILL.md) と同じキャリブレーションガイドラインを使用する:
|
||||
|
||||
* 散文:`words × 1.3`
|
||||
* コード集約またはコード混在/コードブロック:`chars / 4`
|
||||
|
||||
混在コンテンツの場合、支配的なコンテンツタイプを使用し、推定ヒューリスティックスを保持する。
|
||||
|
||||
### ステップ 2 — 複雑度に応じてレスポンスサイズを推定する
|
||||
|
||||
プロンプトを分類し、乗数範囲を適用して完全なレスポンスウィンドウを得る:
|
||||
|
||||
| 複雑度 | 乗数範囲 | プロンプト例 |
|
||||
|--------------|------------|------------------------------------------------------|
|
||||
| シンプル | 3× – 8× | 「Xとは何ですか?」、はい/いいえの質問、単一の事実 |
|
||||
| 中程度 | 8× – 20× | 「Xはどのように機能しますか?」 |
|
||||
| 中〜高 | 10× – 25× | コンテキスト付きのコードリクエスト |
|
||||
| 複雑 | 15× – 40× | マルチパート分析、比較、アーキテクチャ |
|
||||
| クリエイティブ | 10× – 30× | ストーリー、散文、ナラティブライティング |
|
||||
|
||||
レスポンスウィンドウ = `input_tokens × mult_min` から `input_tokens × mult_max`(ただしモデルの設定済み出力トークン制限を超えない)。
|
||||
|
||||
### ステップ 3 — 深さのオプションを提示する
|
||||
|
||||
**回答する前に**、実際に推定した数値を使用してこのブロックを提示する:
|
||||
|
||||
```
|
||||
プロンプトを分析中...
|
||||
|
||||
入力:~[N] トークン | タイプ:[タイプ] | 複雑度:[レベル] | 言語:[言語]
|
||||
|
||||
深さレベルを選択してください:
|
||||
|
||||
[1] ベーシック (25%) -> ~[トークン数] 直接回答、前置きなし
|
||||
[2] 適度 (50%) -> ~[トークン数] 回答 + 背景 + 1つの例
|
||||
[3] 詳細 (75%) -> ~[トークン数] 代替案を含む完全な回答
|
||||
[4] 徹底的 (100%) -> ~[トークン数] すべて、制限なし
|
||||
|
||||
どのレベルを選択しますか?(1-4 または「25%の深さ」「50%の深さ」「75%の深さ」「100%の深さ」)
|
||||
|
||||
精度:ヒューリスティック推定、約85〜90%の精度(±15%)。
|
||||
```
|
||||
|
||||
各レベルのトークン推定(レスポンスウィンドウ内):
|
||||
|
||||
* 25% → `min + (max - min) × 0.25`
|
||||
* 50% → `min + (max - min) × 0.50`
|
||||
* 75% → `min + (max - min) × 0.75`
|
||||
* 100% → `max`
|
||||
|
||||
### ステップ 4 — 選択されたレベルで回答する
|
||||
|
||||
| レベル | 目標の長さ | 含む内容 | 省略する内容 |
|
||||
|------------------|---------------------|-----------------------------------------------------|---------------------------------------------------|
|
||||
| 25% コア | 最大2〜4文 | 直接回答、重要な結論 | コンテキスト、例、ニュアンス、代替案 |
|
||||
| 50% 適度 | 1〜3段落 | 回答 + 必要なコンテキスト + 1つの例 | 深い分析、エッジケース、参考文献 |
|
||||
| 75% 詳細 | 構造化された回答 | 複数の例、長所/短所、代替案 | 極端なエッジケース、網羅的な参考文献 |
|
||||
| 100% 徹底的 | 制限なし | すべて——完全な分析、すべてのコード、すべての視点 | なし |
|
||||
|
||||
## ショートカット——質問をスキップ
|
||||
|
||||
ユーザーがすでにレベルを示している場合、質問せずにそのレベルで即座に回答する:
|
||||
|
||||
| ユーザーの発言 | レベル |
|
||||
|----------------------------------------------------|-------|
|
||||
| 「1」/「25%の深さ」/「短いバージョン」/「簡潔に」/「TL;DR」 | 25% |
|
||||
| 「2」/「50%の深さ」/「適度の深さ」/「バランスの取れた回答」 | 50% |
|
||||
| 「3」/「75%の深さ」/「詳細な回答」/「包括的な回答」 | 75% |
|
||||
| 「4」/「100%の深さ」/「徹底的な回答」/「完全で詳細な分析」 | 100% |
|
||||
|
||||
ユーザーが本セッションですでにレベルを設定している場合、ユーザーが変更しない限り後続の回答も**静かに**そのレベルを維持する。
|
||||
|
||||
## 精度について
|
||||
|
||||
このスキルはヒューリスティック推定を使用する——実際のトークナイザーではない。精度は約85〜90%で偏差は±15%。常に免責事項を表示する。
|
||||
|
||||
## 例
|
||||
|
||||
### トリガーシナリオ
|
||||
|
||||
* 「まず短いバージョンをください。」
|
||||
* 「あなたの回答は何トークン使いますか?」
|
||||
* 「50%の深さで回答してください。」
|
||||
* 「徹底的な回答が欲しい、サマリーはいらない。」
|
||||
* 「まず短いバージョン、次に詳細なバージョンをください。」
|
||||
|
||||
### トリガーしないシナリオ
|
||||
|
||||
* 「JWTトークンとは何ですか?」
|
||||
* 「チェックアウトフローは支払いトークンを使用しています。」
|
||||
* 「これは正常ですか?」
|
||||
* 「リファクタリングを完了してください。」
|
||||
* ユーザーが本セッションの深さを選択した後の後続の質問
|
||||
|
||||
## 出典
|
||||
|
||||
[TBA — Claude CodeのToken Budget Advisor](https://github.com/Xabilimon1/Token-Budget-Advisor-Claude-Code-)から引用した独立スキル。
|
||||
元のプロジェクトにはPython推定スクリプトも付属しているが、本リポジトリではスキルを自己完結型に保ち、ヒューリスティックスのみを使用する。
|
||||
465
docs/ja-JP/skills/ui-demo/SKILL.md
Normal file
465
docs/ja-JP/skills/ui-demo/SKILL.md
Normal file
@@ -0,0 +1,465 @@
|
||||
---
|
||||
name: ui-demo
|
||||
description: Playwrightを使用して美しいUIデモ動画を録画する。ユーザーがWebアプリのデモ、ウォークスルー、スクリーン録画、またはチュートリアル動画の作成を求める場合に使用する。可視カーソル、自然なリズム、プロフェッショナルな仕上がりのWebM動画を生成する。
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# UI デモ動画レコーダー
|
||||
|
||||
Playwrightの動画録画機能を使用して、注入されたカーソルオーバーレイ、自然なリズム、ナラティブフローを備えた美しいWebアプリのデモ動画を録画する。
|
||||
|
||||
## 使用場面
|
||||
|
||||
* ユーザーが「デモ動画」「スクリーン録画」「操作デモ」または「チュートリアル」を求める場合
|
||||
* ユーザーが機能またはワークフローを視覚的に見せたい場合
|
||||
* ユーザーがドキュメント、オンボーディング、ステークホルダーへのデモのために動画が必要な場合
|
||||
|
||||
## 3フェーズのプロセス
|
||||
|
||||
すべてのデモは **探索 -> リハーサル -> 録画** の3つのフェーズを経る。録画フェーズに直接ジャンプしない。
|
||||
|
||||
***
|
||||
|
||||
## フェーズ 1:探索
|
||||
|
||||
スクリプトを書く前に、ターゲットページを探索して実際の内容を把握する。
|
||||
|
||||
### なぜか
|
||||
|
||||
見たことのない内容のスクリプトは書けない。フィールドが `<textarea>` ではなく `<input>` の場合、ドロップダウンが `<select>` ではなくカスタムコンポーネントの場合、コメントボックスが `@mentions` や `#tags` をサポートしている場合があある。仮定は録画を静かに壊す。
|
||||
|
||||
### 方法
|
||||
|
||||
フローの各ページに移動し、インタラクティブな要素をダンプする:
|
||||
|
||||
```javascript
|
||||
// Run this for each page in the flow BEFORE writing the demo script
|
||||
const fields = await page.evaluate(() => {
|
||||
const els = [];
|
||||
document.querySelectorAll('input, select, textarea, button, [contenteditable]').forEach(el => {
|
||||
if (el.offsetParent !== null) {
|
||||
els.push({
|
||||
tag: el.tagName,
|
||||
type: el.type || '',
|
||||
name: el.name || '',
|
||||
placeholder: el.placeholder || '',
|
||||
text: el.textContent?.trim().substring(0, 40) || '',
|
||||
contentEditable: el.contentEditable === 'true',
|
||||
role: el.getAttribute('role') || '',
|
||||
});
|
||||
}
|
||||
});
|
||||
return els;
|
||||
});
|
||||
console.log(JSON.stringify(fields, null, 2));
|
||||
```
|
||||
|
||||
### 確認すべき内容
|
||||
|
||||
* **フォームフィールド**:`<select>`、`<input>`、カスタムドロップダウン、コンボボックスのどれか?
|
||||
* **選択オプション**:オプションの値とテキストをダンプする。プレースホルダーには `value="0"` または `value=""` が含まれることがあり、非空に見える。`Array.from(el.options).map(o => ({ value: o.value, text: o.text }))` を使用する。テキストに「選択」が含まれるオプションや値が `"0"` のオプションをスキップする。
|
||||
* **リッチテキスト**:コメントボックスは `@mentions`、`#tags`、Markdown、絵文字をサポートしているか?プレースホルダーテキストを確認する。
|
||||
* **必須フィールド**:どのフィールドがフォームの送信をブロックするか?ラベルの `required`、`*` を確認し、空のフォームを送信してバリデーションエラーを確認する。
|
||||
* **動的コンテンツ**:他のフィールドを入力した後にフィールドが表示されるか?
|
||||
* **ボタンラベル**:正確なテキスト(`"Submit"`、`"Submit Request"`、`"Send"` など)。
|
||||
* **テーブル列ヘッダー**:テーブル駆動のモーダルには、各 `input[type="number"]` をその列ヘッダーにマッピングする。すべての数値入力が同じ意味を持つと仮定しない。
|
||||
|
||||
### 出力
|
||||
|
||||
スクリプトに正しいセレクターを書くために使用する、ページごとのフィールドマッピング。例:
|
||||
|
||||
```text
|
||||
/purchase-requests/new:
|
||||
- 予算コード: <select>(ページの最初のドロップダウン、4オプション)
|
||||
- 希望納期: <input type="date">
|
||||
- 背景説明: <textarea>(inputではない)
|
||||
- BOMテーブル: インライン編集可能なセル、span.cursor-pointer -> inputパターン
|
||||
- 送信: <button> テキスト="送信"
|
||||
|
||||
/purchase-requests/N(詳細):
|
||||
- コメント: <input placeholder="メッセージを入力...">、@ユーザーと#PRタグに対応
|
||||
- 送信: <button> テキスト="送信"(入力前は無効)
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## フェーズ 2:リハーサル
|
||||
|
||||
録画せずにすべてのステップを実行する。各セレクターが解決されることを確認する。
|
||||
|
||||
### なぜか
|
||||
|
||||
セレクターの失敗は、デモ録画が壊れる最大の原因。リハーサルは録画を無駄にする前に問題を発見する。
|
||||
|
||||
### 方法
|
||||
|
||||
`ensureVisible` を使用する——ログを記録して大きくエラーを報告するラッパー:
|
||||
|
||||
```javascript
|
||||
async function ensureVisible(page, locator, label) {
|
||||
const el = typeof locator === 'string' ? page.locator(locator).first() : locator;
|
||||
const visible = await el.isVisible().catch(() => false);
|
||||
if (!visible) {
|
||||
const msg = `REHEARSAL FAIL: "${label}" not found - selector: ${typeof locator === 'string' ? locator : '(locator object)'}`;
|
||||
console.error(msg);
|
||||
const found = await page.evaluate(() => {
|
||||
return Array.from(document.querySelectorAll('button, input, select, textarea, a'))
|
||||
.filter(el => el.offsetParent !== null)
|
||||
.map(el => `${el.tagName}[${el.type || ''}] "${el.textContent?.trim().substring(0, 30)}"`)
|
||||
.join('\n ');
|
||||
});
|
||||
console.error(' Visible elements:\n ' + found);
|
||||
return false;
|
||||
}
|
||||
console.log(`REHEARSAL OK: "${label}"`);
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
### リハーサルスクリプトの構造
|
||||
|
||||
```javascript
|
||||
const steps = [
|
||||
{ label: 'Login email field', selector: '#email' },
|
||||
{ label: 'Login submit', selector: 'button[type="submit"]' },
|
||||
{ label: 'New Request button', selector: 'button:has-text("New Request")' },
|
||||
{ label: 'Budget Code select', selector: 'select' },
|
||||
{ label: 'Delivery date', selector: 'input[type="date"]:visible' },
|
||||
{ label: 'Description field', selector: 'textarea:visible' },
|
||||
{ label: 'Add Item button', selector: 'button:has-text("Add Item")' },
|
||||
{ label: 'Submit button', selector: 'button:has-text("Submit")' },
|
||||
];
|
||||
|
||||
let allOk = true;
|
||||
for (const step of steps) {
|
||||
if (!await ensureVisible(page, step.selector, step.label)) {
|
||||
allOk = false;
|
||||
}
|
||||
}
|
||||
if (!allOk) {
|
||||
console.error('REHEARSAL FAILED - fix selectors before recording');
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('REHEARSAL PASSED - all selectors verified');
|
||||
```
|
||||
|
||||
### リハーサルが失敗した場合
|
||||
|
||||
1. 可視要素のダンプを読む。
|
||||
2. 正しいセレクターを見つける。
|
||||
3. スクリプトを更新する。
|
||||
4. リハーサルを再実行する。
|
||||
5. すべてのセレクターが通過した後のみ続行する。
|
||||
|
||||
***
|
||||
|
||||
## フェーズ 3:録画
|
||||
|
||||
探索とリハーサルが通過した後にのみ、録画を作成する。
|
||||
|
||||
### 録画の原則
|
||||
|
||||
#### 1. ナラティブフロー
|
||||
|
||||
動画をストーリーとして計画する。ユーザーが指定した順序に従うか、このデフォルト順序を使用する:
|
||||
|
||||
* **エントリー**:ログインまたは開始点へのナビゲーション
|
||||
* **コンテキスト**:周囲を確認して、視聴者がどこにいるか理解できるようにする
|
||||
* **アクション**:主要なワークフローステップを実行する
|
||||
* **バリアント**:設定、テーマ、ローカライゼーションなどの補助機能を表示する
|
||||
* **結果**:結果、確認、または新しい状態を表示する
|
||||
|
||||
#### 2. リズム
|
||||
|
||||
* ログイン後:`4秒`
|
||||
* ナビゲーション後:`3秒`
|
||||
* ボタンクリック後:`2秒`
|
||||
* 主要なステップ間:`1.5〜2秒`
|
||||
* 最終アクション後:`3秒`
|
||||
* 入力の遅延:文字ごとに `25〜40ms`
|
||||
|
||||
#### 3. カーソルオーバーレイ
|
||||
|
||||
マウスの動きを追うSVGの矢印カーソルを注入する:
|
||||
|
||||
```javascript
|
||||
async function injectCursor(page) {
|
||||
await page.evaluate(() => {
|
||||
if (document.getElementById('demo-cursor')) return;
|
||||
const cursor = document.createElement('div');
|
||||
cursor.id = 'demo-cursor';
|
||||
cursor.innerHTML = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5 3L19 12L12 13L9 20L5 3Z" fill="white" stroke="black" stroke-width="1.5" stroke-linejoin="round"/>
|
||||
</svg>`;
|
||||
cursor.style.cssText = `
|
||||
position: fixed; z-index: 999999; pointer-events: none;
|
||||
width: 24px; height: 24px;
|
||||
transition: left 0.1s, top 0.1s;
|
||||
filter: drop-shadow(1px 1px 2px rgba(0,0,0,0.3));
|
||||
`;
|
||||
cursor.style.left = '0px';
|
||||
cursor.style.top = '0px';
|
||||
document.body.appendChild(cursor);
|
||||
document.addEventListener('mousemove', (e) => {
|
||||
cursor.style.left = e.clientX + 'px';
|
||||
cursor.style.top = e.clientY + 'px';
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
オーバーレイはナビゲーション時に破棄されるため、ページナビゲーションのたびに `injectCursor(page)` を呼び出す。
|
||||
|
||||
#### 4. マウスの動き
|
||||
|
||||
カーソルを瞬間移動させない。クリック前にターゲットに移動する:
|
||||
|
||||
```javascript
|
||||
async function moveAndClick(page, locator, label, opts = {}) {
|
||||
const { postClickDelay = 800, ...clickOpts } = opts;
|
||||
const el = typeof locator === 'string' ? page.locator(locator).first() : locator;
|
||||
const visible = await el.isVisible().catch(() => false);
|
||||
if (!visible) {
|
||||
console.error(`WARNING: moveAndClick skipped - "${label}" not visible`);
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
await el.scrollIntoViewIfNeeded();
|
||||
await page.waitForTimeout(300);
|
||||
const box = await el.boundingBox();
|
||||
if (box) {
|
||||
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2, { steps: 10 });
|
||||
await page.waitForTimeout(400);
|
||||
}
|
||||
await el.click(clickOpts);
|
||||
} catch (e) {
|
||||
console.error(`WARNING: moveAndClick failed on "${label}": ${e.message}`);
|
||||
return false;
|
||||
}
|
||||
await page.waitForTimeout(postClickDelay);
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
デバッグのために各呼び出しに説明的な `label` を含める。
|
||||
|
||||
#### 5. 入力
|
||||
|
||||
瞬時に入力するのではなく、目に見えるように入力する:
|
||||
|
||||
```javascript
|
||||
async function typeSlowly(page, locator, text, label, charDelay = 35) {
|
||||
const el = typeof locator === 'string' ? page.locator(locator).first() : locator;
|
||||
const visible = await el.isVisible().catch(() => false);
|
||||
if (!visible) {
|
||||
console.error(`WARNING: typeSlowly skipped - "${label}" not visible`);
|
||||
return false;
|
||||
}
|
||||
await moveAndClick(page, el, label);
|
||||
await el.fill('');
|
||||
await el.pressSequentially(text, { delay: charDelay });
|
||||
await page.waitForTimeout(500);
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
#### 6. スクロール
|
||||
|
||||
ジャンプではなくスムーズスクロールを使用する:
|
||||
|
||||
```javascript
|
||||
await page.evaluate(() => window.scrollTo({ top: 400, behavior: 'smooth' }));
|
||||
await page.waitForTimeout(1500);
|
||||
```
|
||||
|
||||
#### 7. ダッシュボードパン
|
||||
|
||||
ダッシュボードや概要ページを表示する場合、主要な要素の上にカーソルを移動させる:
|
||||
|
||||
```javascript
|
||||
async function panElements(page, selector, maxCount = 6) {
|
||||
const elements = await page.locator(selector).all();
|
||||
for (let i = 0; i < Math.min(elements.length, maxCount); i++) {
|
||||
try {
|
||||
const box = await elements[i].boundingBox();
|
||||
if (box && box.y < 700) {
|
||||
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2, { steps: 8 });
|
||||
await page.waitForTimeout(600);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`WARNING: panElements skipped element ${i} (selector: "${selector}"): ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 8. 字幕
|
||||
|
||||
ビューポートの下部に字幕バーを注入する:
|
||||
|
||||
```javascript
|
||||
async function injectSubtitleBar(page) {
|
||||
await page.evaluate(() => {
|
||||
if (document.getElementById('demo-subtitle')) return;
|
||||
const bar = document.createElement('div');
|
||||
bar.id = 'demo-subtitle';
|
||||
bar.style.cssText = `
|
||||
position: fixed; bottom: 0; left: 0; right: 0; z-index: 999998;
|
||||
text-align: center; padding: 12px 24px;
|
||||
background: rgba(0, 0, 0, 0.75);
|
||||
color: white; font-family: -apple-system, "Segoe UI", sans-serif;
|
||||
font-size: 16px; font-weight: 500; letter-spacing: 0.3px;
|
||||
transition: opacity 0.3s;
|
||||
pointer-events: none;
|
||||
`;
|
||||
bar.textContent = '';
|
||||
bar.style.opacity = '0';
|
||||
document.body.appendChild(bar);
|
||||
});
|
||||
}
|
||||
|
||||
async function showSubtitle(page, text) {
|
||||
await page.evaluate((t) => {
|
||||
const bar = document.getElementById('demo-subtitle');
|
||||
if (!bar) return;
|
||||
if (t) {
|
||||
bar.textContent = t;
|
||||
bar.style.opacity = '1';
|
||||
} else {
|
||||
bar.style.opacity = '0';
|
||||
}
|
||||
}, text);
|
||||
if (text) await page.waitForTimeout(800);
|
||||
}
|
||||
```
|
||||
|
||||
ナビゲーションのたびに `injectSubtitleBar(page)` を `injectCursor(page)` と一緒に呼び出す。
|
||||
|
||||
使用パターン:
|
||||
|
||||
```javascript
|
||||
await showSubtitle(page, 'Step 1 - Logging in');
|
||||
await showSubtitle(page, 'Step 2 - Dashboard overview');
|
||||
await showSubtitle(page, '');
|
||||
```
|
||||
|
||||
ガイドライン:
|
||||
|
||||
* 字幕テキストは短く、60文字以内が望ましい。
|
||||
* 一貫性のために `Step N - Action` 形式を使用する。
|
||||
* 長い一時停止でインターフェースが自己説明的な場合は字幕をクリアする。
|
||||
|
||||
## スクリプトテンプレート
|
||||
|
||||
```javascript
|
||||
'use strict';
|
||||
const { chromium } = require('playwright');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
const BASE_URL = process.env.QA_BASE_URL || 'http://localhost:3000';
|
||||
const VIDEO_DIR = path.join(__dirname, 'screenshots');
|
||||
const OUTPUT_NAME = 'demo-FEATURE.webm';
|
||||
const REHEARSAL = process.argv.includes('--rehearse');
|
||||
|
||||
// Paste injectCursor, injectSubtitleBar, showSubtitle, moveAndClick,
|
||||
// typeSlowly, ensureVisible, and panElements here.
|
||||
|
||||
(async () => {
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
|
||||
if (REHEARSAL) {
|
||||
const context = await browser.newContext({ viewport: { width: 1280, height: 720 } });
|
||||
const page = await context.newPage();
|
||||
// Navigate through the flow and run ensureVisible for each selector.
|
||||
await browser.close();
|
||||
return;
|
||||
}
|
||||
|
||||
const context = await browser.newContext({
|
||||
recordVideo: { dir: VIDEO_DIR, size: { width: 1280, height: 720 } },
|
||||
viewport: { width: 1280, height: 720 }
|
||||
});
|
||||
const page = await context.newPage();
|
||||
|
||||
try {
|
||||
await injectCursor(page);
|
||||
await injectSubtitleBar(page);
|
||||
|
||||
await showSubtitle(page, 'Step 1 - Logging in');
|
||||
// login actions
|
||||
|
||||
await page.goto(`${BASE_URL}/dashboard`);
|
||||
await injectCursor(page);
|
||||
await injectSubtitleBar(page);
|
||||
await showSubtitle(page, 'Step 2 - Dashboard overview');
|
||||
// pan dashboard
|
||||
|
||||
await showSubtitle(page, 'Step 3 - Main workflow');
|
||||
// action sequence
|
||||
|
||||
await showSubtitle(page, 'Step 4 - Result');
|
||||
// final reveal
|
||||
await showSubtitle(page, '');
|
||||
} catch (err) {
|
||||
console.error('DEMO ERROR:', err.message);
|
||||
} finally {
|
||||
await context.close();
|
||||
const video = page.video();
|
||||
if (video) {
|
||||
const src = await video.path();
|
||||
const dest = path.join(VIDEO_DIR, OUTPUT_NAME);
|
||||
try {
|
||||
fs.copyFileSync(src, dest);
|
||||
console.log('Video saved:', dest);
|
||||
} catch (e) {
|
||||
console.error('ERROR: Failed to copy video:', e.message);
|
||||
console.error(' Source:', src);
|
||||
console.error(' Destination:', dest);
|
||||
}
|
||||
}
|
||||
await browser.close();
|
||||
}
|
||||
})();
|
||||
```
|
||||
|
||||
使用方法:
|
||||
|
||||
```bash
|
||||
# Phase 2: Rehearse
|
||||
node demo-script.cjs --rehearse
|
||||
|
||||
# Phase 3: Record
|
||||
node demo-script.cjs
|
||||
```
|
||||
|
||||
## 録画前チェックリスト
|
||||
|
||||
* \[ ] 探索フェーズが完了
|
||||
* \[ ] リハーサルが通過し、すべてのセレクターが機能する
|
||||
* \[ ] ヘッドレスモードが有効
|
||||
* \[ ] 解像度が `1280x720` に設定されている
|
||||
* \[ ] 各ナビゲーション後にカーソルと字幕のオーバーレイを再注入する
|
||||
* \[ ] 主要なトランジション時に `showSubtitle(page, 'Step N - ...')` を使用する
|
||||
* \[ ] すべてのクリックが説明的なラベル付きの `moveAndClick` を使用する
|
||||
* \[ ] 目に見える入力が `typeSlowly` を使用する
|
||||
* \[ ] サイレントキャッチなし。ヘルパー関数は警告を記録する
|
||||
* \[ ] コンテンツ表示にスムーズスクロールを使用する
|
||||
* \[ ] 重要な一時停止が視聴者に対して見える
|
||||
* \[ ] フローが要求されたストーリー順序に従っている
|
||||
* \[ ] スクリプトがフェーズ1で発見した実際のUIを反映している
|
||||
|
||||
## よくある落とし穴
|
||||
|
||||
1. ナビゲーション後にカーソルが消える——再注入する。
|
||||
2. 動画が速すぎる——一時停止を追加する。
|
||||
3. カーソルが矢印ではなく点になっている——SVGオーバーレイを使用する。
|
||||
4. カーソルが瞬間移動する——クリック前に移動する。
|
||||
5. ドロップダウン選択が途切れる——移動を表示してからオプションを選択する。
|
||||
6. モーダルが唐突に見える——確認前に読み取り一時停止を追加する。
|
||||
7. 動画ファイルパスがランダム——安定した出力名にコピーする。
|
||||
8. セレクターの失敗が飲み込まれる——サイレントキャッチブロックを絶対に使わない。
|
||||
9. フィールドタイプを仮定する——まず探索する。
|
||||
10. 機能を仮定する——スクリプトを書く前に実際のUIを確認する。
|
||||
11. プレースホルダーの選択値が本物に見える——`"0"` と `"Select..."` に注意する。
|
||||
12. ポップアップが別の動画を作成する——ポップアップページを明示的にキャプチャし、必要に応じて後でマージする。
|
||||
197
docs/ja-JP/skills/unified-notifications-ops/SKILL.md
Normal file
197
docs/ja-JP/skills/unified-notifications-ops/SKILL.md
Normal file
@@ -0,0 +1,197 @@
|
||||
---
|
||||
name: unified-notifications-ops
|
||||
description: GitHub、Linear、デスクトップアラート、フック、接続された通信インターフェースを網羅する、統合されたECCネイティブワークフローとして通知を運用する。真の問題がアラートルーティング、重複排除、エスカレーション、またはインボックス崩壊である場合に使用する。
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# 統合通知運用
|
||||
|
||||
真の問題が通知の欠如ではなく、通知システムの断片化にある場合にこのスキルを使用する。
|
||||
|
||||
目標は、分散したイベントを単一のオペレーターインターフェースに統合することであり、以下を含む:
|
||||
|
||||
* 明確な重大度レベル
|
||||
* 明確な責任者
|
||||
* 明確なルーティング
|
||||
* 明確な次のアクション
|
||||
|
||||
## 使用する場面
|
||||
|
||||
* ユーザーがGitHub、Linear、ローカルフック、デスクトップアラート、チャット、メール間の統一通知チャネルを望んでいる
|
||||
* CI失敗、レビューリクエスト、Issue更新、オペレーターイベントが各所に散在している
|
||||
* 現在のセットアップがアクションではなくノイズを生成している
|
||||
* ユーザーが重複する通知ブランチや積み残しのプロポーザルを単一のECCネイティブチャネルに統合したい
|
||||
* ワークスペースにフック、MCP、または接続されたツールがあるが、一貫した通知戦略がない
|
||||
|
||||
## 優先インターフェース
|
||||
|
||||
既存のものから始める:
|
||||
|
||||
* GitHub Issues、PR、レビュー、コメント、CI
|
||||
* Linear Issues/プロジェクトのステータス変更
|
||||
* ローカルフックイベントとセッションライフサイクルシグナル
|
||||
* デスクトップ通知プリミティブ
|
||||
* 接続されたメール/チャットインターフェース(実際に存在する場合)
|
||||
|
||||
独立した通知製品をユーザーに勧めるより、ECCネイティブのオーケストレーションを優先する。
|
||||
|
||||
## 絶対的なルール
|
||||
|
||||
* トークン、シークレット、Webhookシークレット、内部識別子を決して公開しない
|
||||
* 以下を区別する:
|
||||
* イベントソース
|
||||
* 重大度レベル
|
||||
* ルーティングチャネル
|
||||
* オペレーターアクション
|
||||
* 中断コストが不明な場合はデフォルトでサマリーファーストアプローチを取る
|
||||
* すべてのチャネルにすべてのイベントをブロードキャストしない
|
||||
* 真の解決策がより良いIssueトリアージ、フック戦略、またはプロジェクトフローである場合は明示する
|
||||
|
||||
## イベントパイプライン
|
||||
|
||||
チャネルを以下として扱う:
|
||||
|
||||
1. **キャプチャ** イベント
|
||||
2. **分類** 緊急度と責任者
|
||||
3. **ルーティング** 適切なチャネルへ
|
||||
4. **マージ** 重複と低シグナルノイズ
|
||||
5. **添付** 次のオペレーターアクション
|
||||
|
||||
目標はより少なく、より良い通知である。
|
||||
|
||||
## デフォルト重大度モデル
|
||||
|
||||
| レベル | 例 | デフォルト処理 |
|
||||
| --- | --- | --- |
|
||||
| クリティカル | デフォルトブランチのCI破損、セキュリティ問題、リリースブロック、デプロイ失敗 | 即座に中断 |
|
||||
| 高 | レビューリクエスト、PR失敗、責任者をブロックするハンドオフ | 当日アラート |
|
||||
| 中 | Issueステータス変更、重要なコメント、バックログ変更 | サマリーまたはキュー |
|
||||
| 低 | 繰り返しの成功、通常のノイズ、冗長なライフサイクルタグ | 抑制または折りたたみ |
|
||||
|
||||
ワークスペースに重大度モデルがない場合は、自動化を提案する前にまず構築する。
|
||||
|
||||
## ワークフロー
|
||||
|
||||
### 1. 現在のインターフェースの棚卸し
|
||||
|
||||
以下を列挙する:
|
||||
|
||||
* イベントソース
|
||||
* 現在のチャネル
|
||||
* アラートを発するフック/スクリプト
|
||||
* 同じイベントの重複パス
|
||||
* 重要事項が表示されないサイレント失敗のケース
|
||||
|
||||
ECCがすでに持っているものを指摘する。
|
||||
|
||||
### 2. 何が中断を正当化するかを決定する
|
||||
|
||||
各イベントファミリーについて答える:
|
||||
|
||||
* 誰が知る必要があるか?
|
||||
* どれくらい早く知る必要があるか?
|
||||
* 中断すべきか、バッチ処理すべきか、ログに記録するだけにすべきか?
|
||||
|
||||
以下のデフォルトを使用する:
|
||||
|
||||
* リリース、CI、セキュリティ、責任者をブロックするイベントは中断
|
||||
* 中程度のシグナル更新にはサマリーを使用
|
||||
* テレメトリと低シグナルライフサイクルタグはログ記録のみ
|
||||
|
||||
### 3. チャネルを追加する前に重複をマージする
|
||||
|
||||
以下を確認する:
|
||||
|
||||
* 同じPRイベントがGitHub、Linear、ローカルログに表示されている
|
||||
* 同じ失敗に対する重複したフック通知
|
||||
* 直接転送するより要約すべきコメントやステータス変更
|
||||
* より良いアクションパスを提供せずに互いを複製しているチャネル
|
||||
|
||||
以下を優先する:
|
||||
|
||||
* 1つの正規サマリー
|
||||
* 1人の責任者
|
||||
* 1つのプライマリチャネル
|
||||
* 1つのフォールバックパス
|
||||
|
||||
### 4. ECCネイティブワークフローを設計する
|
||||
|
||||
各実際の通知ニーズについて定義する:
|
||||
|
||||
* **ソース**
|
||||
* **ゲーティング**
|
||||
* **形式**:即時アラート、サマリー、キュー、またはダッシュボードのみ
|
||||
* **チャネル**
|
||||
* **アクション**
|
||||
|
||||
ECCがすでにプリミティブを持っている場合は優先して使用する:
|
||||
|
||||
* オペレータートリアージスキル
|
||||
* 自動トリガー/実行フック
|
||||
* 委譲されたトリアージのためのエージェント
|
||||
* 本当にブリッジが欠けている場合のみMCP/コネクター
|
||||
|
||||
### 5. アクション指向の設計を返す
|
||||
|
||||
最終出力:
|
||||
|
||||
* 保持するもの
|
||||
* 抑制するもの
|
||||
* マージするもの
|
||||
* ECCが次にカプセル化すべきもの
|
||||
|
||||
## 出力フォーマット
|
||||
|
||||
```text
|
||||
現在のサーフェス
|
||||
- ソース
|
||||
- チャネル
|
||||
- 重複
|
||||
- ギャップ
|
||||
|
||||
イベントモデル
|
||||
- クリティカル
|
||||
- 高
|
||||
- 中
|
||||
- 低
|
||||
|
||||
ルーティング計画
|
||||
- ソース -> チャネル
|
||||
- 理由
|
||||
- オペレーター/担当者
|
||||
|
||||
統合
|
||||
- 抑制
|
||||
- マージ
|
||||
- 正規サマリー
|
||||
|
||||
次のECCアクション
|
||||
- スキル/フック/エージェント/MCP
|
||||
- 次に構築する具体的なワークフロー
|
||||
```
|
||||
|
||||
## 推奨ルール
|
||||
|
||||
* 複数の弱いチャネルより1つの強いチャネルを優先する
|
||||
* 中程度と低シグナルの更新にはサマリーを優先する
|
||||
* シグナルが自動トリガーされるべき場合はフックを優先する
|
||||
* 作業がトリアージ、ルーティング、レビュー決定を伴う場合はオペレータースキルを優先する
|
||||
* 根本原因がアラートではなくバックログ/PR調整である場合は `project-flow-ops` を優先する
|
||||
* ユーザーが最初にソースの棚卸しを必要とする場合は `workspace-surface-audit` を優先する
|
||||
* デスクトップ通知で十分な場合は不要な外部ブリッジを発明しない
|
||||
|
||||
## 良いユースケース
|
||||
|
||||
* 「GitHub、Linear、ローカルフックアラートがあるが、統一されたオペレーターフローがない」
|
||||
* 「CIの失敗ノイズが多くて人々が無視している」
|
||||
* 「Claude、OpenCode、Codexインターフェース全体で統一された通知戦略が欲しい」
|
||||
* 「何を中断すべきで、何をサマリーに入れるべきかを判断してほしい」
|
||||
* 「重複する通知PRのアイデアを1つの正規ECCチャネルに統合してほしい」
|
||||
|
||||
## 関連スキル
|
||||
|
||||
* `workspace-surface-audit`
|
||||
* `project-flow-ops`
|
||||
* `github-ops`
|
||||
* `knowledge-ops`
|
||||
* `customer-billing-ops` 通知の痛みポイントがエンジニアリングではなく課金/顧客運用に関わる場合
|
||||
317
docs/ja-JP/skills/video-editing/SKILL.md
Normal file
317
docs/ja-JP/skills/video-editing/SKILL.md
Normal file
@@ -0,0 +1,317 @@
|
||||
---
|
||||
name: video-editing
|
||||
description: 実写素材のカット、構築、強化のためのAI支援ビデオ編集ワークフロー。生の撮影素材からFFmpeg、Remotion、ElevenLabs、fal.aiを経て、DescriptまたはCapCutで最終仕上げを行う完全なパイプラインをカバーする。ユーザーがビデオの編集、素材のカット、vlogの作成、またはビデオコンテンツの構築を望む場合に使用する。
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# ビデオ編集
|
||||
|
||||
実際の素材に対するAI支援編集。プロンプトからの生成ではない。既存のビデオを素早く編集する。
|
||||
|
||||
## 有効化する場面
|
||||
|
||||
* ユーザーがビデオ素材の編集、カット、または構築をしたい
|
||||
* 長い録音を短いビデオコンテンツに変換する
|
||||
* 生の素材からvlog、チュートリアル、またはデモビデオを構築する
|
||||
* 既存のビデオにオーバーレイ、字幕、音楽、またはナレーションを追加する
|
||||
* 異なるプラットフォーム(YouTube、TikTok、Instagram)用にビデオを再フレーミングする
|
||||
* ユーザーが「ビデオを編集する」「この素材をカットする」「vlogを作る」「ビデオワークフロー」と言及している
|
||||
|
||||
## コアフィロソフィー
|
||||
|
||||
AIにビデオ全体を作成させることをやめ、実際の素材を圧縮・構築・強化するために使い始めると、AI動画編集が役立つようになる。価値は生成にあるのではない。価値は圧縮にある。
|
||||
|
||||
## 処理パイプライン
|
||||
|
||||
```
|
||||
Screen Studio / 生の素材
|
||||
→ Claude / Codex
|
||||
→ FFmpeg
|
||||
→ Remotion
|
||||
→ ElevenLabs / fal.ai
|
||||
→ Descript または CapCut
|
||||
```
|
||||
|
||||
各レイヤーには特定の役割がある。レイヤーをスキップしない。1つのツールですべてをやろうとしない。
|
||||
|
||||
## レイヤー1:収集(Screen Studio / 生の素材)
|
||||
|
||||
ソース素材を収集する:
|
||||
|
||||
* **Screen Studio**:アプリのデモ、コーディングセッション、ブラウザワークフロー向けの洗練されたスクリーンレコーディング
|
||||
* **生のカメラ素材**:vlog素材、インタビュー、イベント録画
|
||||
* **VideoDBによるデスクトップキャプチャ**:リアルタイムコンテキストを伴うセッション録画(`videodb` スキル参照)
|
||||
|
||||
出力:整理準備ができた生のファイル。
|
||||
|
||||
## レイヤー2:整理(Claude / Codex)
|
||||
|
||||
Claude CodeまたはCodexを使用して:
|
||||
|
||||
* **転写とタグ付け**:トランスクリプトを生成し、トピックとキーポイントを特定する
|
||||
* **構造の計画**:保持するもの、カットするもの、順序を決定する
|
||||
* **無効なセグメントの特定**:ポーズ、脱線、テイクの繰り返しを見つける
|
||||
* **編集決定リストの生成**:カット用のタイムスタンプ、保持するセグメント
|
||||
* **FFmpegとRemotionコードのスキャフォールディング**:コマンドとコンポジションを生成する
|
||||
|
||||
```
|
||||
プロンプトの例:
|
||||
「これは4時間の録音のトランスクリプトです。24分のvlogに最適な8つのハイライトを見つけてください。
|
||||
各セグメントにFFmpegカットコマンドを提供してください。」
|
||||
```
|
||||
|
||||
このレイヤーは構造に関するものであり、最終的なクリエイティブな判断ではない。
|
||||
|
||||
## レイヤー3:決定論的カット(FFmpeg)
|
||||
|
||||
FFmpegは退屈だが重要な作業を処理する:分割、トリミング、結合、前処理。
|
||||
|
||||
### タイムスタンプでセグメントを抽出する
|
||||
|
||||
```bash
|
||||
ffmpeg -i raw.mp4 -ss 00:12:30 -to 00:15:45 -c copy segment_01.mp4
|
||||
```
|
||||
|
||||
### 編集決定リストに基づくバッチカット
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# cuts.txt: start,end,label
|
||||
while IFS=, read -r start end label; do
|
||||
ffmpeg -i raw.mp4 -ss "$start" -to "$end" -c copy "segments/${label}.mp4"
|
||||
done < cuts.txt
|
||||
```
|
||||
|
||||
### セグメントを結合する
|
||||
|
||||
```bash
|
||||
# Create file list
|
||||
for f in segments/*.mp4; do echo "file '$f'"; done > concat.txt
|
||||
ffmpeg -f concat -safe 0 -i concat.txt -c copy assembled.mp4
|
||||
```
|
||||
|
||||
### 編集を高速化するためのプロキシファイルを作成する
|
||||
|
||||
```bash
|
||||
ffmpeg -i raw.mp4 -vf "scale=960:-2" -c:v libx264 -preset ultrafast -crf 28 proxy.mp4
|
||||
```
|
||||
|
||||
### 転写用に音声を抽出する
|
||||
|
||||
```bash
|
||||
ffmpeg -i raw.mp4 -vn -acodec pcm_s16le -ar 16000 audio.wav
|
||||
```
|
||||
|
||||
### 音声レベルを正規化する
|
||||
|
||||
```bash
|
||||
ffmpeg -i segment.mp4 -af loudnorm=I=-16:TP=-1.5:LRA=11 -c:v copy normalized.mp4
|
||||
```
|
||||
|
||||
## レイヤー4:プログラマブルコンポジション(Remotion)
|
||||
|
||||
Remotionは編集問題をコンポーザブルなコードに変換する。従来のエディタでは面倒なことに使用する:
|
||||
|
||||
### Remotionを使用する場面
|
||||
|
||||
* オーバーレイ:テキスト、画像、ブランドロゴ、ローワーサード
|
||||
* データビジュアライゼーション:チャート、統計、アニメーション数値
|
||||
* モーショングラフィックス:トランジション、説明アニメーション
|
||||
* コンポーザブルシーン:ビデオ間で再利用可能なテンプレート
|
||||
* 製品デモ:注釈付きスクリーンショット、UIハイライト
|
||||
|
||||
### 基本的なRemotionコンポジション
|
||||
|
||||
```tsx
|
||||
import { AbsoluteFill, Sequence, Video, useCurrentFrame } from "remotion";
|
||||
|
||||
export const VlogComposition: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
|
||||
return (
|
||||
<AbsoluteFill>
|
||||
{/* Main footage */}
|
||||
<Sequence from={0} durationInFrames={300}>
|
||||
<Video src="/segments/intro.mp4" />
|
||||
</Sequence>
|
||||
|
||||
{/* Title overlay */}
|
||||
<Sequence from={30} durationInFrames={90}>
|
||||
<AbsoluteFill style={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}>
|
||||
<h1 style={{
|
||||
fontSize: 72,
|
||||
color: "white",
|
||||
textShadow: "2px 2px 8px rgba(0,0,0,0.8)",
|
||||
}}>
|
||||
The AI Editing Stack
|
||||
</h1>
|
||||
</AbsoluteFill>
|
||||
</Sequence>
|
||||
|
||||
{/* Next segment */}
|
||||
<Sequence from={300} durationInFrames={450}>
|
||||
<Video src="/segments/demo.mp4" />
|
||||
</Sequence>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### 出力をレンダリングする
|
||||
|
||||
```bash
|
||||
npx remotion render src/index.ts VlogComposition output.mp4
|
||||
```
|
||||
|
||||
詳細なパターンとAPIリファレンスについては[Remotionドキュメント](https://www.remotion.dev/docs)を参照する。
|
||||
|
||||
## レイヤー5:生成アセット(ElevenLabs / fal.ai)
|
||||
|
||||
必要なものだけを生成する。ビデオ全体を生成しない。
|
||||
|
||||
### ElevenLabsでのナレーション
|
||||
|
||||
```python
|
||||
import os
|
||||
import requests
|
||||
|
||||
resp = requests.post(
|
||||
f"https://api.elevenlabs.io/v1/text-to-speech/{voice_id}",
|
||||
headers={
|
||||
"xi-api-key": os.environ["ELEVENLABS_API_KEY"],
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
json={
|
||||
"text": "Your narration text here",
|
||||
"model_id": "eleven_turbo_v2_5",
|
||||
"voice_settings": {"stability": 0.5, "similarity_boost": 0.75}
|
||||
}
|
||||
)
|
||||
with open("voiceover.mp3", "wb") as f:
|
||||
f.write(resp.content)
|
||||
```
|
||||
|
||||
### fal.aiでの音楽と効果音の生成
|
||||
|
||||
`fal-ai-media` スキルを以下に使用する:
|
||||
|
||||
* バックグラウンドミュージック生成
|
||||
* 効果音(ビデオからオーディオへのThinkSoundモデル)
|
||||
* トランジション効果音
|
||||
|
||||
### fal.aiでのビジュアル生成
|
||||
|
||||
存在しないカットアウェイ、サムネイル、またはBロール素材に使用する:
|
||||
|
||||
```
|
||||
generate(app_id: "fal-ai/nano-banana-pro", input_data: {
|
||||
"prompt": "プロフェッショナルなテクビデオサムネイル、暗い背景、画面上にコード",
|
||||
"image_size": "landscape_16_9"
|
||||
})
|
||||
```
|
||||
|
||||
### VideoDBによる生成オーディオ
|
||||
|
||||
VideoDBが設定されている場合:
|
||||
|
||||
```python
|
||||
voiceover = coll.generate_voice(text="Narration here", voice="alloy")
|
||||
music = coll.generate_music(prompt="lo-fi background for coding vlog", duration=120)
|
||||
sfx = coll.generate_sound_effect(prompt="subtle whoosh transition")
|
||||
```
|
||||
|
||||
## レイヤー6:最終仕上げ(Descript / CapCut)
|
||||
|
||||
最後のレイヤーは人間が行う。従来のエディタを使用して:
|
||||
|
||||
* **ペーシング調整**:速すぎたり遅すぎると感じるカットを調整する
|
||||
* **字幕**:自動生成してから手動でクリーンアップする
|
||||
* **カラーグレーディング**:基本的な補正とムード調整
|
||||
* **最終オーディオミックス**:ボイス、音楽、効果音のレベルをバランスする
|
||||
* **エクスポート**:プラットフォーム固有のフォーマットと品質設定
|
||||
|
||||
ここにセンスが現れる。AIが繰り返し作業をクリーンアップする。最終的な決定はあなたが行う。
|
||||
|
||||
## ソーシャルメディア向けの再フレーミング
|
||||
|
||||
プラットフォームによって異なるアスペクト比が必要:
|
||||
|
||||
| プラットフォーム | アスペクト比 | 解像度 |
|
||||
|----------|-------------|------------|
|
||||
| YouTube | 16:9 | 1920x1080 |
|
||||
| TikTok / Reels | 9:16 | 1080x1920 |
|
||||
| Instagram Feed | 1:1 | 1080x1080 |
|
||||
| X / Twitter | 16:9 または 1:1 | 1280x720 または 720x720 |
|
||||
|
||||
### FFmpegで再フレーミングする
|
||||
|
||||
```bash
|
||||
# 16:9 to 9:16 (center crop)
|
||||
ffmpeg -i input.mp4 -vf "crop=ih*9/16:ih,scale=1080:1920" vertical.mp4
|
||||
|
||||
# 16:9 to 1:1 (center crop)
|
||||
ffmpeg -i input.mp4 -vf "crop=ih:ih,scale=1080:1080" square.mp4
|
||||
```
|
||||
|
||||
### VideoDBで再フレーミングする
|
||||
|
||||
```python
|
||||
from videodb import ReframeMode
|
||||
|
||||
# Smart reframe (AI-guided subject tracking)
|
||||
reframed = video.reframe(start=0, end=60, target="vertical", mode=ReframeMode.smart)
|
||||
```
|
||||
|
||||
## シーン検出と自動カット
|
||||
|
||||
### FFmpegシーン検出
|
||||
|
||||
```bash
|
||||
# Detect scene changes (threshold 0.3 = moderate sensitivity)
|
||||
ffmpeg -i input.mp4 -vf "select='gt(scene,0.3)',showinfo" -vsync vfr -f null - 2>&1 | grep showinfo
|
||||
```
|
||||
|
||||
### 自動カットのための無音検出
|
||||
|
||||
```bash
|
||||
# Find silent segments (useful for cutting dead air)
|
||||
ffmpeg -i input.mp4 -af silencedetect=noise=-30dB:d=2 -f null - 2>&1 | grep silence
|
||||
```
|
||||
|
||||
### ハイライト抽出
|
||||
|
||||
Claudeを使用してトランスクリプト+シーンタイムスタンプを分析する:
|
||||
|
||||
```
|
||||
「タイムスタンプ付きのトランスクリプトとシーントランジションポイントに基づいて、
|
||||
ソーシャルメディア投稿に最適な5つの30秒の最も魅力的なクリップを見つけてください。」
|
||||
```
|
||||
|
||||
## 各ツールが最も得意とすること
|
||||
|
||||
| ツール | 強み | 弱み |
|
||||
|------|----------|----------|
|
||||
| Claude / Codex | 整理、計画、コード生成 | クリエイティブな判断レイヤーではない |
|
||||
| FFmpeg | 決定論的カット、バッチ処理、フォーマット変換 | ビジュアル編集UIなし |
|
||||
| Remotion | プログラマブルオーバーレイ、コンポーザブルシーン、再利用可能テンプレート | 非開発者には学習曲線がある |
|
||||
| Screen Studio | 即座に洗練されたスクリーンレコーディングを取得 | スクリーンキャプチャのみ |
|
||||
| ElevenLabs | ボイス、ナレーション、音楽、効果音 | ワークフローのコアではない |
|
||||
| Descript / CapCut | 最終ペーシング調整、字幕、仕上げ | 手動操作、自動化不可 |
|
||||
|
||||
## 主要原則
|
||||
|
||||
1. **生成ではなく編集。** このワークフローは実際の素材をカットするためのものであり、プロンプトから作成するものではない。
|
||||
2. **スタイルより先に構造。** ビジュアル要素に触れる前に、レイヤー2でストーリー構造を確定させる。
|
||||
3. **FFmpegが骨格。** 退屈だが重要。長い素材がここで管理可能になる。
|
||||
4. **Remotionは再現性のために。** 何度も行う操作はRemotionコンポーネントにする。
|
||||
5. **選択的な生成。** 存在しないアセットにのみAI生成を使用し、すべてには使用しない。
|
||||
6. **センスは最後のレイヤー。** AIが繰り返し作業をクリーンアップする。最終的なクリエイティブな決定はあなたが行う。
|
||||
|
||||
## 関連スキル
|
||||
|
||||
* `fal-ai-media` — AI画像、ビデオ、オーディオ生成
|
||||
* `videodb` — サーバーサイドのビデオ処理、インデックス作成、ストリーミング
|
||||
* `content-engine` — プラットフォームネイティブなコンテンツ配信
|
||||
386
docs/ja-JP/skills/videodb/SKILL.md
Normal file
386
docs/ja-JP/skills/videodb/SKILL.md
Normal file
@@ -0,0 +1,386 @@
|
||||
---
|
||||
name: videodb
|
||||
description: ビデオとオーディオの表示、理解、アクション。表示:ローカルファイル、URL、RTSP/ライブストリーム、またはリアルタイムのデスクトップ録画からコンテンツを取得し、リアルタイムコンテキストと再生可能なストリームリンクを返す。理解:フレームを抽出し、ビジュアル/セマンティック/時間的インデックスを構築し、タイムスタンプと自動クリップでモーメントを検索する。アクション:トランスコードと正規化(コーデック、フレームレート、解像度、アスペクト比)、タイムライン編集(字幕、テキスト/画像オーバーレイ、ブランディング、オーディオオーバーレイ、吹き替え、翻訳)、メディアアセットの生成(画像、オーディオ、ビデオ)、ライブストリームまたはデスクトップキャプチャされたイベントのリアルタイムアラートを実行する。
|
||||
origin: ECC
|
||||
allowed-tools: Read Grep Glob Bash(python:*)
|
||||
argument-hint: "[task description]"
|
||||
---
|
||||
|
||||
# VideoDBスキル
|
||||
|
||||
**ビデオ、ライブストリーム、デスクトップセッションのための知覚 + 記憶 + アクション。**
|
||||
|
||||
## ユースケース
|
||||
|
||||
### デスクトップ知覚
|
||||
|
||||
* **デスクトップセッション**を開始/停止し、**画面、マイク、システムオーディオ**をキャプチャする
|
||||
* **リアルタイムコンテキスト**をストリーミングし、**セグメント化されたセッション記憶**を保存する
|
||||
* 言われた内容と画面上で起きていることに対して**リアルタイムアラート/トリガー**を実行する
|
||||
* **セッションサマリー**、検索可能なタイムライン、**再生可能な証拠リンク**を生成する
|
||||
|
||||
### ビデオ取り込み + ストリーミング
|
||||
|
||||
* **ファイルまたはURL**を取り込み、**再生可能なウェブストリームリンク**を返す
|
||||
* トランスコード/正規化:**コーデック、ビットレート、フレームレート、解像度、アスペクト比**
|
||||
|
||||
### インデックス + 検索(タイムスタンプ + 証拠)
|
||||
|
||||
* **ビジュアル**、**音声**、**キーワード**インデックスを構築する
|
||||
* **タイムスタンプ**と**再生可能な証拠**で正確なモーメントを検索して返す
|
||||
* 検索結果から自動的に**クリップ**を作成する
|
||||
|
||||
### タイムライン編集 + 生成
|
||||
|
||||
* 字幕:**生成**、**翻訳**、**バーンイン**
|
||||
* オーバーレイ:**テキスト/画像/ブランドロゴ**、動的キャプション
|
||||
* オーディオ:**バックグラウンドミュージック**、**ナレーション**、**吹き替え**
|
||||
* **タイムライン操作**によるプログラマティックなコンポジションとエクスポート
|
||||
|
||||
### ライブストリーム(RTSP)+ 監視
|
||||
|
||||
* **RTSP/ライブストリーム**に接続する
|
||||
* **リアルタイムのビジュアルと音声理解**を実行し、監視ワークフロー向けに**イベント/アラート**を発する
|
||||
|
||||
## 仕組み
|
||||
|
||||
### 一般的な入力
|
||||
|
||||
* ローカル**ファイルパス**、公開**URL**、または**RTSP URL**
|
||||
* デスクトップキャプチャリクエスト:**開始 / 停止 / セッションのサマリー作成**
|
||||
* 目的のアクション:理解コンテキストの取得、トランスコード仕様、インデックス仕様、検索クエリ、クリップ範囲、タイムライン編集、アラートルール
|
||||
|
||||
### 一般的な出力
|
||||
|
||||
* **ストリームURL**
|
||||
* **タイムスタンプ**と**証拠リンク**付きの検索結果
|
||||
* 生成されたアセット:字幕、オーディオ、画像、クリップ
|
||||
* ライブストリーム向け**イベント/アラートペイロード**
|
||||
* デスクトップ**セッションサマリー**と記憶エントリ
|
||||
|
||||
### Pythonコードの実行
|
||||
|
||||
VideoDBコードを実行する前に、プロジェクトディレクトリに移動して環境変数をロードする:
|
||||
|
||||
```python
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv(".env")
|
||||
|
||||
import videodb
|
||||
conn = videodb.connect()
|
||||
```
|
||||
|
||||
これにより以下から `VIDEO_DB_API_KEY` が読み込まれる:
|
||||
|
||||
1. 環境変数(エクスポートされている場合)
|
||||
2. プロジェクトの現在のディレクトリにある `.env` ファイル
|
||||
|
||||
キーが欠けている場合、`videodb.connect()` は自動的に `AuthenticationError` を発生させる。
|
||||
|
||||
短いインラインコマンドで十分な場合はスクリプトファイルを書かない。
|
||||
|
||||
インラインPython (`python -c "..."`) を書く場合は、常に適切にフォーマットされたコードを使用する——セミコロンで文を区切り、読みやすくする。約3文以上の場合はheredocを使用する:
|
||||
|
||||
```bash
|
||||
python << 'EOF'
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv(".env")
|
||||
|
||||
import videodb
|
||||
conn = videodb.connect()
|
||||
coll = conn.get_collection()
|
||||
print(f"Videos: {len(coll.get_videos())}")
|
||||
EOF
|
||||
```
|
||||
|
||||
### セットアップ
|
||||
|
||||
ユーザーが「videodbのセットアップ」などを要求した場合:
|
||||
|
||||
### 1. SDKのインストール
|
||||
|
||||
```bash
|
||||
pip install "videodb[capture]" python-dotenv
|
||||
```
|
||||
|
||||
Linuxで `videodb[capture]` が失敗する場合は、キャプチャ拡張なしでインストールする:
|
||||
|
||||
```bash
|
||||
pip install videodb python-dotenv
|
||||
```
|
||||
|
||||
### 2. APIキーの設定
|
||||
|
||||
ユーザーは**いずれかの**方法で `VIDEO_DB_API_KEY` を設定する必要がある:
|
||||
|
||||
* **ターミナルでエクスポート**(Claudeを起動する前に):`export VIDEO_DB_API_KEY=your-key`
|
||||
* **プロジェクトの `.env` ファイル**:プロジェクトの `.env` ファイルに `VIDEO_DB_API_KEY=your-key` を保存する
|
||||
|
||||
APIキーを無料で取得するには [console.videodb.io](https://console.videodb.io)(クレジットカード不要で50回の無料アップロード)を訪問する。
|
||||
|
||||
APIキーを自分で読み取り、書き込み、または処理**しない**。常にユーザーが設定するようにする。
|
||||
|
||||
### クイックリファレンス
|
||||
|
||||
### メディアのアップロード
|
||||
|
||||
```python
|
||||
# URL
|
||||
video = coll.upload(url="https://example.com/video.mp4")
|
||||
|
||||
# YouTube
|
||||
video = coll.upload(url="https://www.youtube.com/watch?v=VIDEO_ID")
|
||||
|
||||
# Local file
|
||||
video = coll.upload(file_path="/path/to/video.mp4")
|
||||
```
|
||||
|
||||
### 転写 + 字幕
|
||||
|
||||
```python
|
||||
# force=True skips the error if the video is already indexed
|
||||
video.index_spoken_words(force=True)
|
||||
text = video.get_transcript_text()
|
||||
stream_url = video.add_subtitle()
|
||||
```
|
||||
|
||||
### ビデオ内検索
|
||||
|
||||
```python
|
||||
from videodb.exceptions import InvalidRequestError
|
||||
|
||||
video.index_spoken_words(force=True)
|
||||
|
||||
# search() raises InvalidRequestError when no results are found.
|
||||
# Always wrap in try/except and treat "No results found" as empty.
|
||||
try:
|
||||
results = video.search("product demo")
|
||||
shots = results.get_shots()
|
||||
stream_url = results.compile()
|
||||
except InvalidRequestError as e:
|
||||
if "No results found" in str(e):
|
||||
shots = []
|
||||
else:
|
||||
raise
|
||||
```
|
||||
|
||||
### シーン検索
|
||||
|
||||
```python
|
||||
import re
|
||||
from videodb import SearchType, IndexType, SceneExtractionType
|
||||
from videodb.exceptions import InvalidRequestError
|
||||
|
||||
# index_scenes() has no force parameter — it raises an error if a scene
|
||||
# index already exists. Extract the existing index ID from the error.
|
||||
try:
|
||||
scene_index_id = video.index_scenes(
|
||||
extraction_type=SceneExtractionType.shot_based,
|
||||
prompt="Describe the visual content in this scene.",
|
||||
)
|
||||
except Exception as e:
|
||||
match = re.search(r"id\s+([a-f0-9]+)", str(e))
|
||||
if match:
|
||||
scene_index_id = match.group(1)
|
||||
else:
|
||||
raise
|
||||
|
||||
# Use score_threshold to filter low-relevance noise (recommended: 0.3+)
|
||||
try:
|
||||
results = video.search(
|
||||
query="person writing on a whiteboard",
|
||||
search_type=SearchType.semantic,
|
||||
index_type=IndexType.scene,
|
||||
scene_index_id=scene_index_id,
|
||||
score_threshold=0.3,
|
||||
)
|
||||
shots = results.get_shots()
|
||||
stream_url = results.compile()
|
||||
except InvalidRequestError as e:
|
||||
if "No results found" in str(e):
|
||||
shots = []
|
||||
else:
|
||||
raise
|
||||
```
|
||||
|
||||
### タイムライン編集
|
||||
|
||||
**重要:** タイムラインを構築する前に必ずタイムスタンプを検証する:
|
||||
|
||||
* `start` は >= 0 でなければならない(負の値は静かに受け入れられるが、破損した出力を生成する)
|
||||
* `start` は `end` より小さくなければならない
|
||||
* `end` は `video.length` 以下でなければならない
|
||||
|
||||
```python
|
||||
from videodb.timeline import Timeline
|
||||
from videodb.asset import VideoAsset, TextAsset, TextStyle
|
||||
|
||||
timeline = Timeline(conn)
|
||||
timeline.add_inline(VideoAsset(asset_id=video.id, start=10, end=30))
|
||||
timeline.add_overlay(0, TextAsset(text="The End", duration=3, style=TextStyle(fontsize=36)))
|
||||
stream_url = timeline.generate_stream()
|
||||
```
|
||||
|
||||
### ビデオのトランスコード(解像度/品質変更)
|
||||
|
||||
```python
|
||||
from videodb import TranscodeMode, VideoConfig, AudioConfig
|
||||
|
||||
# Change resolution, quality, or aspect ratio server-side
|
||||
job_id = conn.transcode(
|
||||
source="https://example.com/video.mp4",
|
||||
callback_url="https://example.com/webhook",
|
||||
mode=TranscodeMode.economy,
|
||||
video_config=VideoConfig(resolution=720, quality=23, aspect_ratio="16:9"),
|
||||
audio_config=AudioConfig(mute=False),
|
||||
)
|
||||
```
|
||||
|
||||
### アスペクト比の調整(ソーシャルプラットフォーム向け)
|
||||
|
||||
**警告:** `reframe()` は低速なサーバーサイド操作。長いビデオでは数分かかる場合があり、タイムアウトする可能性がある。ベストプラクティス:
|
||||
|
||||
* 可能な限り `start`/`end` を使用して短いセグメントに制限する
|
||||
* フルレングスビデオには非同期処理のために `callback_url` を使用する
|
||||
* まず `Timeline` でビデオをトリミングし、短い結果のアスペクト比を調整する
|
||||
|
||||
```python
|
||||
from videodb import ReframeMode
|
||||
|
||||
# Always prefer reframing a short segment:
|
||||
reframed = video.reframe(start=0, end=60, target="vertical", mode=ReframeMode.smart)
|
||||
|
||||
# Async reframe for full-length videos (returns None, result via webhook):
|
||||
video.reframe(target="vertical", callback_url="https://example.com/webhook")
|
||||
|
||||
# Presets: "vertical" (9:16), "square" (1:1), "landscape" (16:9)
|
||||
reframed = video.reframe(start=0, end=60, target="square")
|
||||
|
||||
# Custom dimensions
|
||||
reframed = video.reframe(start=0, end=60, target={"width": 1280, "height": 720})
|
||||
```
|
||||
|
||||
### 生成メディア
|
||||
|
||||
```python
|
||||
image = coll.generate_image(
|
||||
prompt="a sunset over mountains",
|
||||
aspect_ratio="16:9",
|
||||
)
|
||||
```
|
||||
|
||||
## エラーハンドリング
|
||||
|
||||
```python
|
||||
from videodb.exceptions import AuthenticationError, InvalidRequestError
|
||||
|
||||
try:
|
||||
conn = videodb.connect()
|
||||
except AuthenticationError:
|
||||
print("Check your VIDEO_DB_API_KEY")
|
||||
|
||||
try:
|
||||
video = coll.upload(url="https://example.com/video.mp4")
|
||||
except InvalidRequestError as e:
|
||||
print(f"Upload failed: {e}")
|
||||
```
|
||||
|
||||
### よくある問題
|
||||
|
||||
| シナリオ | エラーメッセージ | 解決策 |
|
||||
|----------|--------------|----------|
|
||||
| 既にインデックスされたビデオのインデックス作成 | `Spoken word index for video already exists` | `video.index_spoken_words(force=True)` を使用してインデックス済みをスキップ |
|
||||
| シーンインデックスが既に存在 | `Scene index with id XXXX already exists` | `re.search(r"id\s+([a-f0-9]+)", str(e))` を使用してエラーから既存の `scene_index_id` を抽出 |
|
||||
| 検索結果なし | `InvalidRequestError: No results found` | 例外をキャッチして空の結果として扱う (`shots = []`) |
|
||||
| アスペクト比調整タイムアウト | 長いビデオで無期限にブロック | `start`/`end` でセグメントを制限するか、非同期処理のために `callback_url` を渡す |
|
||||
| タイムライン上の負のタイムスタンプ | 破損したストリームを静かに生成 | `VideoAsset` を作成する前に常に `start >= 0` を検証する |
|
||||
| `generate_video()` / `create_collection()` の失敗 | `Operation not allowed` または `maximum limit` | プラン制限された機能——ユーザーにプラン制限を通知する |
|
||||
|
||||
## 例
|
||||
|
||||
### 標準的なプロンプト
|
||||
|
||||
* 「デスクトップキャプチャを開始し、パスワードフィールドが表示されたときにアラートを発する。」
|
||||
* 「セッションを記録して終了時に実行可能なサマリーを生成する。」
|
||||
* 「このファイルを取り込んで再生可能なストリームリンクを返す。」
|
||||
* 「このフォルダをインデックス化して、人物がいるすべてのシーンを見つけ、タイムスタンプを返す。」
|
||||
* 「字幕を生成してバーンインし、軽いバックグラウンドミュージックを追加する。」
|
||||
* 「このRTSP URLに接続して、誰かがエリアに入ったときにアラートを発する。」
|
||||
|
||||
### スクリーンレコーディング(デスクトップキャプチャ)
|
||||
|
||||
`ws_listener.py` を使用して録画セッション中にWebSocketイベントをキャプチャする。デスクトップキャプチャは**macOS**のみサポート。
|
||||
|
||||
#### クイックスタート
|
||||
|
||||
1. **状態ディレクトリを選択**:`STATE_DIR="${VIDEODB_EVENTS_DIR:-$HOME/.local/state/videodb}"`
|
||||
2. **リスナーを起動**:`VIDEODB_EVENTS_DIR="$STATE_DIR" python scripts/ws_listener.py --clear "$STATE_DIR" &`
|
||||
3. **WebSocket IDを取得**:`cat "$STATE_DIR/videodb_ws_id"`
|
||||
4. **キャプチャコードを実行**(完全なワークフローはreference/capture.mdを参照)
|
||||
5. **イベントの書き込み先**:`$STATE_DIR/videodb_events.jsonl`
|
||||
|
||||
新しいキャプチャ実行を開始するときは常に `--clear` を使用して、古い転写とビジュアルイベントが新しいセッションに漏れないようにする。
|
||||
|
||||
#### イベントのクエリ
|
||||
|
||||
```python
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
events_dir = Path(os.environ.get("VIDEODB_EVENTS_DIR", Path.home() / ".local" / "state" / "videodb"))
|
||||
events_file = events_dir / "videodb_events.jsonl"
|
||||
events = []
|
||||
|
||||
if events_file.exists():
|
||||
with events_file.open(encoding="utf-8") as handle:
|
||||
for line in handle:
|
||||
try:
|
||||
events.append(json.loads(line))
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
transcripts = [e["data"]["text"] for e in events if e.get("channel") == "transcript"]
|
||||
cutoff = time.time() - 300
|
||||
recent_visual = [
|
||||
e for e in events
|
||||
if e.get("channel") == "visual_index" and e["unix_ts"] > cutoff
|
||||
]
|
||||
```
|
||||
|
||||
## 追加ドキュメント
|
||||
|
||||
参考ドキュメントはこのSKILL.mdファイルと同じディレクトリの `reference/` ディレクトリにある。必要に応じてGlobツールを使用して見つける。
|
||||
|
||||
* [reference/api-reference.md](reference/api-reference.md) - 完全なVideoDB Python SDK APIリファレンス
|
||||
* [reference/search.md](reference/search.md) - ビデオ検索の詳細ガイド(音声とシーンベース)
|
||||
* [reference/editor.md](reference/editor.md) - タイムライン編集、アセット、コンポジション
|
||||
* [reference/streaming.md](reference/streaming.md) - HLSストリーミングと即時再生
|
||||
* [reference/generative.md](reference/generative.md) - AI駆動のメディア生成(画像、ビデオ、オーディオ)
|
||||
* [reference/rtstream.md](reference/rtstream.md) - ライブストリーム取り込みワークフロー(RTSP/RTMP)
|
||||
* [reference/rtstream-reference.md](reference/rtstream-reference.md) - RTStream SDKメソッドとAIパイプライン
|
||||
* [reference/capture.md](reference/capture.md) - デスクトップキャプチャワークフロー
|
||||
* [reference/capture-reference.md](reference/capture-reference.md) - Capture SDKとWebSocketイベント
|
||||
* [reference/use-cases.md](reference/use-cases.md) - 一般的なビデオ処理パターンと例
|
||||
|
||||
**VideoDBがその操作をサポートする場合、ffmpeg、moviepy、またはローカルエンコーディングツールを使用しない。** 以下のすべての操作はVideoDBによってサーバーサイドで処理される——トリミング、クリップのマージ、オーディオや音楽のオーバーレイ、字幕の追加、テキスト/画像オーバーレイ、トランスコード、解像度変更、アスペクト比変換、プラットフォーム要件へのリサイズ、転写、メディア生成。reference/editor.mdの「制限」セクションに記載されている操作(トランジション、速度変更、クロップ/ズーム、カラーグレーディング、音量ミキシング)の場合のみローカルツールにフォールバックする。
|
||||
|
||||
### 何を使うべきか
|
||||
|
||||
| 問題 | VideoDBソリューション |
|
||||
|---------|-----------------|
|
||||
| プラットフォームがビデオのアスペクト比または解像度を拒否 | `VideoConfig` を使用した `video.reframe()` または `conn.transcode()` |
|
||||
| Twitter/Instagram/TikTok向けにビデオをリサイズする必要がある | `video.reframe(target="vertical")` または `target="square"` |
|
||||
| 解像度を変更する必要がある(例:1080p → 720p) | `VideoConfig(resolution=720)` を使用した `conn.transcode()` |
|
||||
| ビデオにオーディオ/音楽をオーバーレイする必要がある | `Timeline` で `AudioAsset` を使用 |
|
||||
| 字幕を追加する必要がある | `video.add_subtitle()` または `CaptionAsset` |
|
||||
| クリップをマージ/トリミングする必要がある | `Timeline` で `VideoAsset` を使用 |
|
||||
| ナレーション、音楽、効果音を生成する必要がある | `coll.generate_voice()`、`generate_music()`、`generate_sound_effect()` |
|
||||
|
||||
## ソース
|
||||
|
||||
このスキルの参考資料は `skills/videodb/reference/` の下でローカルに提供されている。
|
||||
実行時に外部リポジトリリンクをたどるのではなく、上記のローカルコピーを使用する。
|
||||
|
||||
**メンテナー:** [VideoDB](https://www.videodb.io/)
|
||||
550
docs/ja-JP/skills/videodb/reference/api-reference.md
Normal file
550
docs/ja-JP/skills/videodb/reference/api-reference.md
Normal file
@@ -0,0 +1,550 @@
|
||||
# 完全APIリファレンス
|
||||
|
||||
VideoDBスキルの参考資料。使用ガイドとワークフロー選択については、[../SKILL.md](../SKILL.md) から始めること。
|
||||
|
||||
## 接続
|
||||
|
||||
```python
|
||||
import videodb
|
||||
|
||||
conn = videodb.connect(
|
||||
api_key="your-api-key", # or set VIDEO_DB_API_KEY env var
|
||||
base_url=None, # custom API endpoint (optional)
|
||||
)
|
||||
```
|
||||
|
||||
**戻り値:** `Connection` オブジェクト
|
||||
|
||||
### 接続メソッド
|
||||
|
||||
| メソッド | 戻り値 | 説明 |
|
||||
|--------|---------|-------------|
|
||||
| `conn.get_collection(collection_id="default")` | `Collection` | コレクションを取得する(IDなしの場合はデフォルトコレクションを取得) |
|
||||
| `conn.get_collections()` | `list[Collection]` | すべてのコレクションを一覧表示する |
|
||||
| `conn.create_collection(name, description, is_public=False)` | `Collection` | 新しいコレクションを作成する |
|
||||
| `conn.update_collection(id, name, description)` | `Collection` | コレクションを更新する |
|
||||
| `conn.check_usage()` | `dict` | アカウントの使用状況統計を取得する |
|
||||
| `conn.upload(source, media_type, name, ...)` | `Video\|Audio\|Image` | デフォルトコレクションにアップロードする |
|
||||
| `conn.record_meeting(meeting_url, bot_name, ...)` | `Meeting` | ミーティングを録画する |
|
||||
| `conn.create_capture_session(...)` | `CaptureSession` | キャプチャセッションを作成する([capture-reference.md](capture-reference.md)参照) |
|
||||
| `conn.youtube_search(query, result_threshold, duration)` | `list[dict]` | YouTubeを検索する |
|
||||
| `conn.transcode(source, callback_url, mode, ...)` | `str` | ビデオをトランスコードする(ジョブIDを返す) |
|
||||
| `conn.get_transcode_details(job_id)` | `dict` | トランスコードジョブの状態と詳細を取得する |
|
||||
| `conn.connect_websocket(collection_id)` | `WebSocketConnection` | WebSocketに接続する([capture-reference.md](capture-reference.md)参照) |
|
||||
|
||||
### トランスコード
|
||||
|
||||
カスタム解像度、品質、オーディオ設定でURLからビデオをトランスコードする。処理はサーバーサイドで行われる——ローカルのffmpegは不要。
|
||||
|
||||
```python
|
||||
from videodb import TranscodeMode, VideoConfig, AudioConfig
|
||||
|
||||
job_id = conn.transcode(
|
||||
source="https://example.com/video.mp4",
|
||||
callback_url="https://example.com/webhook",
|
||||
mode=TranscodeMode.economy,
|
||||
video_config=VideoConfig(resolution=720, quality=23),
|
||||
audio_config=AudioConfig(mute=False),
|
||||
)
|
||||
```
|
||||
|
||||
#### transcodeのパラメータ
|
||||
|
||||
| パラメータ | 型 | デフォルト | 説明 |
|
||||
|-----------|------|---------|-------------|
|
||||
| `source` | `str` | 必須 | トランスコードするビデオURL(ダウンロード可能なURLが望ましい) |
|
||||
| `callback_url` | `str` | 必須 | トランスコード完了時にコールバックを受信するURL |
|
||||
| `mode` | `TranscodeMode` | `TranscodeMode.economy` | トランスコード速度:`economy` または `lightning` |
|
||||
| `video_config` | `VideoConfig` | `VideoConfig()` | ビデオエンコード設定 |
|
||||
| `audio_config` | `AudioConfig` | `AudioConfig()` | オーディオエンコード設定 |
|
||||
|
||||
ジョブID (`str`) を返す。`conn.get_transcode_details(job_id)` を使用してジョブの状態を確認する。
|
||||
|
||||
```python
|
||||
details = conn.get_transcode_details(job_id)
|
||||
```
|
||||
|
||||
#### VideoConfig
|
||||
|
||||
```python
|
||||
from videodb import VideoConfig, ResizeMode
|
||||
|
||||
config = VideoConfig(
|
||||
resolution=720, # Target resolution height (e.g. 480, 720, 1080)
|
||||
quality=23, # Encoding quality (lower = better, default 23)
|
||||
framerate=30, # Target framerate
|
||||
aspect_ratio="16:9", # Target aspect ratio
|
||||
resize_mode=ResizeMode.crop, # How to fit: crop, fit, or pad
|
||||
)
|
||||
```
|
||||
|
||||
| フィールド | 型 | デフォルト | 説明 |
|
||||
|-------|------|---------|-------------|
|
||||
| `resolution` | `int\|None` | `None` | ターゲット解像度の高さ(ピクセル) |
|
||||
| `quality` | `int` | `23` | エンコード品質(低いほど高品質) |
|
||||
| `framerate` | `int\|None` | `None` | ターゲットフレームレート |
|
||||
| `aspect_ratio` | `str\|None` | `None` | ターゲットアスペクト比(例:`"16:9"`, `"9:16"`) |
|
||||
| `resize_mode` | `str` | `ResizeMode.crop` | リサイズ戦略:`crop`, `fit`, または `pad` |
|
||||
|
||||
#### AudioConfig
|
||||
|
||||
```python
|
||||
from videodb import AudioConfig
|
||||
|
||||
config = AudioConfig(mute=False)
|
||||
```
|
||||
|
||||
| フィールド | 型 | デフォルト | 説明 |
|
||||
|-------|------|---------|-------------|
|
||||
| `mute` | `bool` | `False` | オーディオトラックをミュートする |
|
||||
|
||||
## コレクション
|
||||
|
||||
```python
|
||||
coll = conn.get_collection()
|
||||
```
|
||||
|
||||
### コレクションメソッド
|
||||
|
||||
| メソッド | 戻り値 | 説明 |
|
||||
|--------|---------|-------------|
|
||||
| `coll.get_videos()` | `list[Video]` | すべてのビデオを一覧表示する |
|
||||
| `coll.get_video(video_id)` | `Video` | 特定のビデオを取得する |
|
||||
| `coll.get_audios()` | `list[Audio]` | すべてのオーディオを一覧表示する |
|
||||
| `coll.get_audio(audio_id)` | `Audio` | 特定のオーディオを取得する |
|
||||
| `coll.get_images()` | `list[Image]` | すべての画像を一覧表示する |
|
||||
| `coll.get_image(image_id)` | `Image` | 特定の画像を取得する |
|
||||
| `coll.upload(url=None, file_path=None, media_type=None, name=None)` | `Video\|Audio\|Image` | メディアをアップロードする |
|
||||
| `coll.search(query, search_type, index_type, score_threshold, namespace, scene_index_id, ...)` | `SearchResult` | コレクション内を検索する(セマンティック検索のみ;キーワードとシーン検索は `NotImplementedError` を発生させる) |
|
||||
| `coll.generate_image(prompt, aspect_ratio="1:1")` | `Image` | AIで画像を生成する |
|
||||
| `coll.generate_video(prompt, duration=5)` | `Video` | AIでビデオを生成する |
|
||||
| `coll.generate_music(prompt, duration=5)` | `Audio` | AIで音楽を生成する |
|
||||
| `coll.generate_sound_effect(prompt, duration=2)` | `Audio` | 効果音を生成する |
|
||||
| `coll.generate_voice(text, voice_name="Default")` | `Audio` | テキストから音声を生成する |
|
||||
| `coll.generate_text(prompt, model_name="basic", response_type="text")` | `dict` | LLMテキスト生成——`["output"]` で結果にアクセス |
|
||||
| `coll.dub_video(video_id, language_code)` | `Video` | ビデオを別の言語に吹き替える |
|
||||
| `coll.record_meeting(meeting_url, bot_name, ...)` | `Meeting` | ライブミーティングを録画する |
|
||||
| `coll.create_capture_session(...)` | `CaptureSession` | キャプチャセッションを作成する([capture-reference.md](capture-reference.md)参照) |
|
||||
| `coll.get_capture_session(...)` | `CaptureSession` | キャプチャセッションを取得する([capture-reference.md](capture-reference.md)参照) |
|
||||
| `coll.connect_rtstream(url, name, ...)` | `RTStream` | ライブストリームに接続する([rtstream-reference.md](rtstream-reference.md)参照) |
|
||||
| `coll.make_public()` | `None` | コレクションを公開にする |
|
||||
| `coll.make_private()` | `None` | コレクションを非公開にする |
|
||||
| `coll.delete_video(video_id)` | `None` | ビデオを削除する |
|
||||
| `coll.delete_audio(audio_id)` | `None` | オーディオを削除する |
|
||||
| `coll.delete_image(image_id)` | `None` | 画像を削除する |
|
||||
| `coll.delete()` | `None` | コレクションを削除する |
|
||||
|
||||
### アップロードのパラメータ
|
||||
|
||||
```python
|
||||
video = coll.upload(
|
||||
url=None, # Remote URL (HTTP, YouTube)
|
||||
file_path=None, # Local file path
|
||||
media_type=None, # "video", "audio", or "image" (auto-detected if omitted)
|
||||
name=None, # Custom name for the media
|
||||
description=None, # Description
|
||||
callback_url=None, # Webhook URL for async notification
|
||||
)
|
||||
```
|
||||
|
||||
## ビデオオブジェクト
|
||||
|
||||
```python
|
||||
video = coll.get_video(video_id)
|
||||
```
|
||||
|
||||
### ビデオ属性
|
||||
|
||||
| 属性 | 型 | 説明 |
|
||||
|----------|------|-------------|
|
||||
| `video.id` | `str` | 一意のビデオID |
|
||||
| `video.collection_id` | `str` | 親コレクションID |
|
||||
| `video.name` | `str` | ビデオ名 |
|
||||
| `video.description` | `str` | ビデオの説明 |
|
||||
| `video.length` | `float` | 長さ(秒) |
|
||||
| `video.stream_url` | `str` | デフォルトのストリームURL |
|
||||
| `video.player_url` | `str` | プレーヤー埋め込みURL |
|
||||
| `video.thumbnail_url` | `str` | サムネイルURL |
|
||||
|
||||
### ビデオメソッド
|
||||
|
||||
| メソッド | 戻り値 | 説明 |
|
||||
|--------|---------|-------------|
|
||||
| `video.generate_stream(timeline=None)` | `str` | ストリームURLを生成する(オプションの `[(start, end)]` タプルタイムライン) |
|
||||
| `video.play()` | `str` | ブラウザでストリームを開き、プレーヤーURLを返す |
|
||||
| `video.index_spoken_words(language_code=None, force=False)` | `None` | 音声検索用にインデックスを作成する。既にインデックス済みの場合は `force=True` でスキップ。 |
|
||||
| `video.index_scenes(extraction_type, prompt, extraction_config, metadata, model_name, name, scenes, callback_url)` | `str` | ビジュアルシーンをインデックス化する(scene\_index\_idを返す) |
|
||||
| `video.index_visuals(prompt, batch_config, ...)` | `str` | ビジュアルコンテンツをインデックス化する(scene\_index\_idを返す) |
|
||||
| `video.index_audio(prompt, model_name, ...)` | `str` | LLMを使用してオーディオをインデックス化する(scene\_index\_idを返す) |
|
||||
| `video.get_transcript(start=None, end=None)` | `list[dict]` | タイムスタンプ付きのトランスクリプトを取得する |
|
||||
| `video.get_transcript_text(start=None, end=None)` | `str` | 完全なトランスクリプトテキストを取得する |
|
||||
| `video.generate_transcript(force=None)` | `dict` | トランスクリプトを生成する |
|
||||
| `video.translate_transcript(language, additional_notes)` | `list[dict]` | トランスクリプトを翻訳する |
|
||||
| `video.search(query, search_type, index_type, filter, **kwargs)` | `SearchResult` | ビデオ内を検索する |
|
||||
| `video.add_subtitle(style=SubtitleStyle())` | `str` | 字幕を追加する(ストリームURLを返す) |
|
||||
| `video.generate_thumbnail(time=None)` | `str\|Image` | サムネイルを生成する |
|
||||
| `video.get_thumbnails()` | `list[Image]` | すべてのサムネイルを取得する |
|
||||
| `video.extract_scenes(extraction_type, extraction_config)` | `SceneCollection` | シーンを抽出する |
|
||||
| `video.reframe(start, end, target, mode, callback_url)` | `Video\|None` | ビデオのアスペクト比を調整する |
|
||||
| `video.clip(prompt, content_type, model_name)` | `str` | プロンプトに基づいてクリップを生成する(ストリームURLを返す) |
|
||||
| `video.insert_video(video, timestamp)` | `str` | タイムスタンプにビデオを挿入する |
|
||||
| `video.download(name=None)` | `dict` | ビデオをダウンロードする |
|
||||
| `video.delete()` | `None` | ビデオを削除する |
|
||||
|
||||
### アスペクト比の調整
|
||||
|
||||
ビデオを異なるアスペクト比に変換する。オプションでスマートオブジェクト追跡を使用。処理はサーバーサイドで行われる。
|
||||
|
||||
> **警告:** アスペクト比の調整は低速なサーバーサイド操作。長いビデオでは数分かかる場合があり、タイムアウトする可能性がある。常に `start`/`end` でセグメントを制限するか、非同期処理のために `callback_url` を渡すこと。
|
||||
|
||||
```python
|
||||
from videodb import ReframeMode
|
||||
|
||||
# Always prefer short segments to avoid timeouts:
|
||||
reframed = video.reframe(start=0, end=60, target="vertical", mode=ReframeMode.smart)
|
||||
|
||||
# Async reframe for full-length videos (returns None, result via webhook):
|
||||
video.reframe(target="vertical", callback_url="https://example.com/webhook")
|
||||
|
||||
# Custom dimensions
|
||||
reframed = video.reframe(start=0, end=60, target={"width": 1080, "height": 1080})
|
||||
```
|
||||
|
||||
#### reframeのパラメータ
|
||||
|
||||
| パラメータ | 型 | デフォルト | 説明 |
|
||||
|-----------|------|---------|-------------|
|
||||
| `start` | `float\|None` | `None` | 開始時間(秒)(None = 開始) |
|
||||
| `end` | `float\|None` | `None` | 終了時間(秒)(None = ビデオ終了) |
|
||||
| `target` | `str\|dict` | `"vertical"` | プリセット文字列(`"vertical"`, `"square"`, `"landscape"`)または `{"width": int, "height": int}` |
|
||||
| `mode` | `str` | `ReframeMode.smart` | `"simple"`(中央クロップ)または `"smart"`(オブジェクト追跡) |
|
||||
| `callback_url` | `str\|None` | `None` | 非同期通知のWebhook URL |
|
||||
|
||||
`callback_url` が提供されない場合は `Video` オブジェクトを返し、そうでない場合は `None` を返す。
|
||||
|
||||
## オーディオオブジェクト
|
||||
|
||||
```python
|
||||
audio = coll.get_audio(audio_id)
|
||||
```
|
||||
|
||||
### オーディオ属性
|
||||
|
||||
| 属性 | 型 | 説明 |
|
||||
|----------|------|-------------|
|
||||
| `audio.id` | `str` | 一意のオーディオID |
|
||||
| `audio.collection_id` | `str` | 親コレクションID |
|
||||
| `audio.name` | `str` | オーディオ名 |
|
||||
| `audio.length` | `float` | 長さ(秒) |
|
||||
|
||||
### オーディオメソッド
|
||||
|
||||
| メソッド | 戻り値 | 説明 |
|
||||
|--------|---------|-------------|
|
||||
| `audio.generate_url()` | `str` | 再生用の署名付きURLを生成する |
|
||||
| `audio.get_transcript(start=None, end=None)` | `list[dict]` | タイムスタンプ付きのトランスクリプトを取得する |
|
||||
| `audio.get_transcript_text(start=None, end=None)` | `str` | 完全なトランスクリプトテキストを取得する |
|
||||
| `audio.generate_transcript(force=None)` | `dict` | トランスクリプトを生成する |
|
||||
| `audio.delete()` | `None` | オーディオを削除する |
|
||||
|
||||
## 画像オブジェクト
|
||||
|
||||
```python
|
||||
image = coll.get_image(image_id)
|
||||
```
|
||||
|
||||
### 画像属性
|
||||
|
||||
| 属性 | 型 | 説明 |
|
||||
|----------|------|-------------|
|
||||
| `image.id` | `str` | 一意の画像ID |
|
||||
| `image.collection_id` | `str` | 親コレクションID |
|
||||
| `image.name` | `str` | 画像名 |
|
||||
| `image.url` | `str\|None` | 画像URL(生成された画像の場合は `None` になる可能性がある——代わりに `generate_url()` を使用) |
|
||||
|
||||
### 画像メソッド
|
||||
|
||||
| メソッド | 戻り値 | 説明 |
|
||||
|--------|---------|-------------|
|
||||
| `image.generate_url()` | `str` | 署名付きURLを生成する |
|
||||
| `image.delete()` | `None` | 画像を削除する |
|
||||
|
||||
## タイムラインとエディター
|
||||
|
||||
### タイムライン
|
||||
|
||||
```python
|
||||
from videodb.timeline import Timeline
|
||||
|
||||
timeline = Timeline(conn)
|
||||
```
|
||||
|
||||
| メソッド | 戻り値 | 説明 |
|
||||
|--------|---------|-------------|
|
||||
| `timeline.add_inline(asset)` | `None` | メイントラックに `VideoAsset` を順番に追加する |
|
||||
| `timeline.add_overlay(start, asset)` | `None` | タイムスタンプに `AudioAsset`、`ImageAsset`、または `TextAsset` をオーバーレイする |
|
||||
| `timeline.generate_stream()` | `str` | コンパイルしてストリームURLを取得する |
|
||||
|
||||
### アセットタイプ
|
||||
|
||||
#### VideoAsset
|
||||
|
||||
```python
|
||||
from videodb.asset import VideoAsset
|
||||
|
||||
asset = VideoAsset(
|
||||
asset_id=video.id,
|
||||
start=0, # trim start (seconds)
|
||||
end=None, # trim end (seconds, None = full)
|
||||
)
|
||||
```
|
||||
|
||||
#### AudioAsset
|
||||
|
||||
```python
|
||||
from videodb.asset import AudioAsset
|
||||
|
||||
asset = AudioAsset(
|
||||
asset_id=audio.id,
|
||||
start=0,
|
||||
end=None,
|
||||
disable_other_tracks=True, # mute original audio when True
|
||||
fade_in_duration=0, # seconds (max 5)
|
||||
fade_out_duration=0, # seconds (max 5)
|
||||
)
|
||||
```
|
||||
|
||||
#### ImageAsset
|
||||
|
||||
```python
|
||||
from videodb.asset import ImageAsset
|
||||
|
||||
asset = ImageAsset(
|
||||
asset_id=image.id,
|
||||
duration=None, # display duration (seconds)
|
||||
width=100, # display width
|
||||
height=100, # display height
|
||||
x=80, # horizontal position (px from left)
|
||||
y=20, # vertical position (px from top)
|
||||
)
|
||||
```
|
||||
|
||||
#### TextAsset
|
||||
|
||||
```python
|
||||
from videodb.asset import TextAsset, TextStyle
|
||||
|
||||
asset = TextAsset(
|
||||
text="Hello World",
|
||||
duration=5,
|
||||
style=TextStyle(
|
||||
fontsize=24,
|
||||
fontcolor="black",
|
||||
boxcolor="white", # background box colour
|
||||
alpha=1.0,
|
||||
font="Sans",
|
||||
text_align="T", # text alignment within box
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
#### CaptionAsset(エディターAPI)
|
||||
|
||||
CaptionAssetはエディターAPIに属し、独自のタイムライン、トラック、クリップシステムを持つ:
|
||||
|
||||
```python
|
||||
from videodb.editor import CaptionAsset, FontStyling
|
||||
|
||||
asset = CaptionAsset(
|
||||
src="auto", # "auto" or base64 ASS string
|
||||
font=FontStyling(name="Clear Sans", size=30),
|
||||
primary_color="&H00FFFFFF",
|
||||
)
|
||||
```
|
||||
|
||||
完全なCaptionAssetの使用方法については、[editor.md](../../../../../skills/videodb/reference/editor.md#caption-overlays) のエディターAPIを参照。
|
||||
|
||||
## ビデオ検索パラメータ
|
||||
|
||||
```python
|
||||
results = video.search(
|
||||
query="your query",
|
||||
search_type=SearchType.semantic, # semantic, keyword, or scene
|
||||
index_type=IndexType.spoken_word, # spoken_word or scene
|
||||
result_threshold=None, # max number of results
|
||||
score_threshold=None, # minimum relevance score
|
||||
dynamic_score_percentage=None, # percentage of dynamic score
|
||||
scene_index_id=None, # target a specific scene index (pass via **kwargs)
|
||||
filter=[], # metadata filters for scene search
|
||||
)
|
||||
```
|
||||
|
||||
> **注意:** `filter` は `video.search()` の明示的な名前付きパラメータ。`scene_index_id` は `**kwargs` を通じてAPIに渡される。
|
||||
>
|
||||
> **重要:** `video.search()` は一致するものがない場合に `"No results found"` というメッセージとともに `InvalidRequestError` を発生させる。常に検索呼び出しをtry/exceptで包むこと。シーン検索には低関連性のノイズをフィルタリングするために `score_threshold=0.3` 以上を使用する。
|
||||
|
||||
シーン検索には `search_type=SearchType.semantic` を使用し `index_type=IndexType.scene` を設定する。特定のシーンインデックスを対象にする場合は `scene_index_id` を渡す。詳細は [search.md](search.md) を参照。
|
||||
|
||||
## SearchResultオブジェクト
|
||||
|
||||
```python
|
||||
results = video.search("query", search_type=SearchType.semantic)
|
||||
```
|
||||
|
||||
| メソッド | 戻り値 | 説明 |
|
||||
|--------|---------|-------------|
|
||||
| `results.get_shots()` | `list[Shot]` | 一致したクリップのリストを取得する |
|
||||
| `results.compile()` | `str` | すべてのショットをストリームURLにコンパイルする |
|
||||
| `results.play()` | `str` | ブラウザでコンパイルされたストリームを開く |
|
||||
|
||||
### Shot属性
|
||||
|
||||
| 属性 | 型 | 説明 |
|
||||
|----------|------|-------------|
|
||||
| `shot.video_id` | `str` | ソースビデオID |
|
||||
| `shot.video_length` | `float` | ソースビデオの長さ |
|
||||
| `shot.video_title` | `str` | ソースビデオのタイトル |
|
||||
| `shot.start` | `float` | 開始時間(秒) |
|
||||
| `shot.end` | `float` | 終了時間(秒) |
|
||||
| `shot.text` | `str` | 一致したテキストコンテンツ |
|
||||
| `shot.search_score` | `float` | 検索関連スコア |
|
||||
|
||||
| メソッド | 戻り値 | 説明 |
|
||||
|--------|---------|-------------|
|
||||
| `shot.generate_stream()` | `str` | この特定のショットをストリーミングする |
|
||||
| `shot.play()` | `str` | ブラウザでショットストリームを開く |
|
||||
|
||||
## Meetingオブジェクト
|
||||
|
||||
```python
|
||||
meeting = coll.record_meeting(
|
||||
meeting_url="https://meet.google.com/...",
|
||||
bot_name="Bot",
|
||||
callback_url=None, # Webhook URL for status updates
|
||||
callback_data=None, # Optional dict passed through to callbacks
|
||||
time_zone="UTC", # Time zone for the meeting
|
||||
)
|
||||
```
|
||||
|
||||
### Meeting属性
|
||||
|
||||
| 属性 | 型 | 説明 |
|
||||
|----------|------|-------------|
|
||||
| `meeting.id` | `str` | 一意のミーティングID |
|
||||
| `meeting.collection_id` | `str` | 親コレクションID |
|
||||
| `meeting.status` | `str` | 現在の状態 |
|
||||
| `meeting.video_id` | `str` | 録画ビデオID(完了後) |
|
||||
| `meeting.bot_name` | `str` | ボット名 |
|
||||
| `meeting.meeting_title` | `str` | ミーティングタイトル |
|
||||
| `meeting.meeting_url` | `str` | ミーティングURL |
|
||||
| `meeting.speaker_timeline` | `dict` | 発言者タイムラインデータ |
|
||||
| `meeting.is_active` | `bool` | 初期化中または処理中の場合はtrue |
|
||||
| `meeting.is_completed` | `bool` | 完了した場合はtrue |
|
||||
|
||||
### Meetingメソッド
|
||||
|
||||
| メソッド | 戻り値 | 説明 |
|
||||
|--------|---------|-------------|
|
||||
| `meeting.refresh()` | `Meeting` | サーバーからデータをリフレッシュする |
|
||||
| `meeting.wait_for_status(target_status, timeout=14400, interval=120)` | `bool` | 指定された状態になるまでポーリングする |
|
||||
|
||||
## RTStreamとCapture
|
||||
|
||||
RTStream(ライブ取り込み、インデックス作成、転写)については [rtstream-reference.md](rtstream-reference.md) を参照。
|
||||
|
||||
キャプチャセッション(デスクトップ録画、CaptureClient、チャネル)については [capture-reference.md](capture-reference.md) を参照。
|
||||
|
||||
## 列挙型と定数
|
||||
|
||||
### SearchType
|
||||
|
||||
```python
|
||||
from videodb import SearchType
|
||||
|
||||
SearchType.semantic # Natural language semantic search
|
||||
SearchType.keyword # Exact keyword matching
|
||||
SearchType.scene # Visual scene search (may require paid plan)
|
||||
SearchType.llm # LLM-powered search
|
||||
```
|
||||
|
||||
### SceneExtractionType
|
||||
|
||||
```python
|
||||
from videodb import SceneExtractionType
|
||||
|
||||
SceneExtractionType.shot_based # Automatic shot boundary detection
|
||||
SceneExtractionType.time_based # Fixed time interval extraction
|
||||
SceneExtractionType.transcript # Transcript-based scene extraction
|
||||
```
|
||||
|
||||
### SubtitleStyle
|
||||
|
||||
```python
|
||||
from videodb import SubtitleStyle
|
||||
|
||||
style = SubtitleStyle(
|
||||
font_name="Arial",
|
||||
font_size=18,
|
||||
primary_colour="&H00FFFFFF",
|
||||
bold=False,
|
||||
# ... see SubtitleStyle for all options
|
||||
)
|
||||
video.add_subtitle(style=style)
|
||||
```
|
||||
|
||||
### SubtitleAlignmentとSubtitleBorderStyle
|
||||
|
||||
```python
|
||||
from videodb import SubtitleAlignment, SubtitleBorderStyle
|
||||
```
|
||||
|
||||
### TextStyle
|
||||
|
||||
```python
|
||||
from videodb import TextStyle
|
||||
# or: from videodb.asset import TextStyle
|
||||
|
||||
style = TextStyle(
|
||||
fontsize=24,
|
||||
fontcolor="black",
|
||||
boxcolor="white",
|
||||
font="Sans",
|
||||
text_align="T",
|
||||
alpha=1.0,
|
||||
)
|
||||
```
|
||||
|
||||
### その他の定数
|
||||
|
||||
```python
|
||||
from videodb import (
|
||||
IndexType, # spoken_word, scene
|
||||
MediaType, # video, audio, image
|
||||
Segmenter, # word, sentence, time
|
||||
SegmentationType, # sentence, llm
|
||||
TranscodeMode, # economy, lightning
|
||||
ResizeMode, # crop, fit, pad
|
||||
ReframeMode, # simple, smart
|
||||
RTStreamChannelType,
|
||||
)
|
||||
```
|
||||
|
||||
## 例外
|
||||
|
||||
```python
|
||||
from videodb.exceptions import (
|
||||
AuthenticationError, # Invalid or missing API key
|
||||
InvalidRequestError, # Bad parameters or malformed request
|
||||
RequestTimeoutError, # Request timed out
|
||||
SearchError, # Search operation failure (e.g. not indexed)
|
||||
VideodbError, # Base exception for all VideoDB errors
|
||||
)
|
||||
```
|
||||
|
||||
| 例外 | よくある原因 |
|
||||
|-----------|-------------|
|
||||
| `AuthenticationError` | 欠落または無効な `VIDEO_DB_API_KEY` |
|
||||
| `InvalidRequestError` | 無効なURL、サポートされていないフォーマット、不正なパラメータ |
|
||||
| `RequestTimeoutError` | サーバーの応答に時間がかかりすぎた |
|
||||
| `SearchError` | インデックス化前の検索、無効な検索タイプ |
|
||||
| `VideodbError` | サーバーエラー、ネットワーク問題、一般的な障害 |
|
||||
416
docs/ja-JP/skills/videodb/reference/capture-reference.md
Normal file
416
docs/ja-JP/skills/videodb/reference/capture-reference.md
Normal file
@@ -0,0 +1,416 @@
|
||||
# キャプチャリファレンス
|
||||
|
||||
VideoDBキャプチャセッションのコードレベルの詳細。ワークフローガイドは [capture.md](capture.md) を参照。
|
||||
|
||||
***
|
||||
|
||||
## WebSocketイベント
|
||||
|
||||
キャプチャセッションとAIパイプラインからのリアルタイムイベント。WebhookやポーリングLiveEventを使用しない。
|
||||
|
||||
[scripts/ws\_listener.py](../../../../../skills/videodb/scripts/ws_listener.py) を使用して接続し、イベントを `${VIDEODB_EVENTS_DIR:-$HOME/.local/state/videodb}/videodb_events.jsonl` にダンプする。
|
||||
|
||||
### イベントチャネル
|
||||
|
||||
| チャネル | ソース | コンテンツ |
|
||||
|---------|--------|---------|
|
||||
| `capture_session` | セッションライフサイクル | 状態変更 |
|
||||
| `transcript` | `start_transcript()` | 音声テキスト変換 |
|
||||
| `visual_index` / `scene_index` | `index_visuals()` | ビジュアル分析 |
|
||||
| `audio_index` | `index_audio()` | オーディオ分析 |
|
||||
| `alert` | `create_alert()` | アラート通知 |
|
||||
|
||||
### セッションライフサイクルイベント
|
||||
|
||||
| イベント | 状態 | 主要データ |
|
||||
|-------|--------|----------|
|
||||
| `capture_session.created` | `created` | — |
|
||||
| `capture_session.starting` | `starting` | — |
|
||||
| `capture_session.active` | `active` | `rtstreams[]` |
|
||||
| `capture_session.stopping` | `stopping` | — |
|
||||
| `capture_session.stopped` | `stopped` | — |
|
||||
| `capture_session.exported` | `exported` | `exported_video_id`, `stream_url`, `player_url` |
|
||||
| `capture_session.failed` | `failed` | `error` |
|
||||
|
||||
### イベント構造
|
||||
|
||||
**転写イベント:**
|
||||
|
||||
```json
|
||||
{
|
||||
"channel": "transcript",
|
||||
"rtstream_id": "rts-xxx",
|
||||
"rtstream_name": "mic:default",
|
||||
"data": {
|
||||
"text": "Let's schedule the meeting for Thursday",
|
||||
"is_final": true,
|
||||
"start": 1710000001234,
|
||||
"end": 1710000002345
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**ビジュアルインデックスイベント:**
|
||||
|
||||
```json
|
||||
{
|
||||
"channel": "visual_index",
|
||||
"rtstream_id": "rts-xxx",
|
||||
"rtstream_name": "display:1",
|
||||
"data": {
|
||||
"text": "User is viewing a Slack conversation with 3 unread messages",
|
||||
"start": 1710000012340,
|
||||
"end": 1710000018900
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**オーディオインデックスイベント:**
|
||||
|
||||
```json
|
||||
{
|
||||
"channel": "audio_index",
|
||||
"rtstream_id": "rts-xxx",
|
||||
"rtstream_name": "mic:default",
|
||||
"data": {
|
||||
"text": "Discussion about scheduling a team meeting",
|
||||
"start": 1710000021500,
|
||||
"end": 1710000029200
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**セッションアクティブイベント:**
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "capture_session.active",
|
||||
"capture_session_id": "cap-xxx",
|
||||
"status": "active",
|
||||
"data": {
|
||||
"rtstreams": [
|
||||
{ "rtstream_id": "rts-1", "name": "mic:default", "media_types": ["audio"] },
|
||||
{ "rtstream_id": "rts-2", "name": "system_audio:default", "media_types": ["audio"] },
|
||||
{ "rtstream_id": "rts-3", "name": "display:1", "media_types": ["video"] }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**セッションエクスポートイベント:**
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "capture_session.exported",
|
||||
"capture_session_id": "cap-xxx",
|
||||
"status": "exported",
|
||||
"data": {
|
||||
"exported_video_id": "v_xyz789",
|
||||
"stream_url": "https://stream.videodb.io/...",
|
||||
"player_url": "https://console.videodb.io/player?url=..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> 最新の詳細については [VideoDB リアルタイムコンテキストドキュメント](https://docs.videodb.io/pages/ingest/capture-sdks/realtime-context.md) を参照。
|
||||
|
||||
***
|
||||
|
||||
## イベント永続化
|
||||
|
||||
`ws_listener.py` を使用してすべてのWebSocketイベントをJSONLファイルにダンプして後で分析する。
|
||||
|
||||
### リスナーを起動してWebSocket IDを取得する
|
||||
|
||||
```bash
|
||||
# Start with --clear to clear old events (recommended for new sessions)
|
||||
python scripts/ws_listener.py --clear &
|
||||
|
||||
# Append to existing events (for reconnects)
|
||||
python scripts/ws_listener.py &
|
||||
```
|
||||
|
||||
またはカスタム出力ディレクトリを指定する:
|
||||
|
||||
```bash
|
||||
python scripts/ws_listener.py --clear /path/to/output &
|
||||
# Or via environment variable:
|
||||
VIDEODB_EVENTS_DIR=/path/to/output python scripts/ws_listener.py --clear &
|
||||
```
|
||||
|
||||
スクリプトは最初の行に `WS_ID=<connection_id>` を出力し、その後無限にリッスンする。
|
||||
|
||||
**ws\_idを取得する:**
|
||||
|
||||
```bash
|
||||
cat "${VIDEODB_EVENTS_DIR:-$HOME/.local/state/videodb}/videodb_ws_id"
|
||||
```
|
||||
|
||||
**リスナーを停止する:**
|
||||
|
||||
```bash
|
||||
kill "$(cat "${VIDEODB_EVENTS_DIR:-$HOME/.local/state/videodb}/videodb_ws_pid")"
|
||||
```
|
||||
|
||||
**`ws_connection_id` を受け入れる関数:**
|
||||
|
||||
| 関数 | 目的 |
|
||||
|----------|---------|
|
||||
| `conn.create_capture_session()` | セッションライフサイクルイベント |
|
||||
| RTStreamメソッド | [rtstream-reference.md](rtstream-reference.md) を参照 |
|
||||
|
||||
**出力ファイル**(出力ディレクトリ内、デフォルトは `${XDG_STATE_HOME:-$HOME/.local/state}/videodb`):
|
||||
|
||||
* `videodb_ws_id` - WebSocket接続ID
|
||||
* `videodb_events.jsonl` - すべてのイベント
|
||||
* `videodb_ws_pid` - 停止用のプロセスID
|
||||
|
||||
**機能:**
|
||||
|
||||
* 起動時にイベントファイルをクリアするための `--clear` フラグ(新しいセッション用)
|
||||
* 接続が切れた場合の指数バックオフによる自動再接続
|
||||
* SIGINT/SIGTERMでのグレースフルシャットダウン
|
||||
* 接続状態のログ記録
|
||||
|
||||
### JSONLフォーマット
|
||||
|
||||
各行はタイムスタンプが付加されたJSONオブジェクト:
|
||||
|
||||
```json
|
||||
{"ts": "2026-03-02T10:15:30.123Z", "unix_ts": 1772446530.123, "channel": "visual_index", "data": {"text": "..."}}
|
||||
{"ts": "2026-03-02T10:15:31.456Z", "unix_ts": 1772446531.456, "event": "capture_session.active", "capture_session_id": "cap-xxx"}
|
||||
```
|
||||
|
||||
### イベントの読み取り
|
||||
|
||||
```python
|
||||
import json
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
events_path = Path.home() / ".local" / "state" / "videodb" / "videodb_events.jsonl"
|
||||
transcripts = []
|
||||
recent = []
|
||||
visual = []
|
||||
|
||||
cutoff = time.time() - 600
|
||||
with events_path.open(encoding="utf-8") as handle:
|
||||
for line in handle:
|
||||
event = json.loads(line)
|
||||
if event.get("channel") == "transcript":
|
||||
transcripts.append(event)
|
||||
if event.get("unix_ts", 0) > cutoff:
|
||||
recent.append(event)
|
||||
if (
|
||||
event.get("channel") == "visual_index"
|
||||
and "code" in event.get("data", {}).get("text", "").lower()
|
||||
):
|
||||
visual.append(event)
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## WebSocket接続
|
||||
|
||||
転写とインデックスパイプラインからリアルタイムのAI結果を受信するために接続する。
|
||||
|
||||
```python
|
||||
ws_wrapper = conn.connect_websocket()
|
||||
ws = await ws_wrapper.connect()
|
||||
ws_id = ws.connection_id
|
||||
```
|
||||
|
||||
| 属性 / メソッド | 型 | 説明 |
|
||||
|-------------------|------|-------------|
|
||||
| `ws.connection_id` | `str` | 一意の接続ID(AIパイプラインメソッドに渡す) |
|
||||
| `ws.receive()` | `AsyncIterator[dict]` | リアルタイムメッセージを生成する非同期イテレータ |
|
||||
|
||||
***
|
||||
|
||||
## CaptureSession
|
||||
|
||||
### 接続メソッド
|
||||
|
||||
| メソッド | 戻り値 | 説明 |
|
||||
|--------|---------|-------------|
|
||||
| `conn.create_capture_session(end_user_id, collection_id, ws_connection_id, metadata)` | `CaptureSession` | 新しいキャプチャセッションを作成する |
|
||||
| `conn.get_capture_session(capture_session_id)` | `CaptureSession` | 既存のキャプチャセッションを取得する |
|
||||
| `conn.generate_client_token()` | `str` | クライアント認証トークンを生成する |
|
||||
|
||||
### キャプチャセッションの作成
|
||||
|
||||
```python
|
||||
from pathlib import Path
|
||||
|
||||
ws_id = (Path.home() / ".local" / "state" / "videodb" / "videodb_ws_id").read_text().strip()
|
||||
|
||||
session = conn.create_capture_session(
|
||||
end_user_id="user-123", # required
|
||||
collection_id="default",
|
||||
ws_connection_id=ws_id,
|
||||
metadata={"app": "my-app"},
|
||||
)
|
||||
print(f"Session ID: {session.id}")
|
||||
```
|
||||
|
||||
> **注意:** `end_user_id` は必須で、キャプチャを開始するユーザーを識別するために使用される。テストやデモ目的には任意の一意の文字列識別子が有効(例:`"demo-user"`、`"test-123"`)。
|
||||
|
||||
### CaptureSession属性
|
||||
|
||||
| 属性 | 型 | 説明 |
|
||||
|----------|------|-------------|
|
||||
| `session.id` | `str` | 一意のキャプチャセッションID |
|
||||
|
||||
### CaptureSessionメソッド
|
||||
|
||||
| メソッド | 戻り値 | 説明 |
|
||||
|--------|---------|-------------|
|
||||
| `session.get_rtstream(type)` | `list[RTStream]` | タイプ別にRTStreamを取得:`"mic"`、`"screen"`、または `"system_audio"` |
|
||||
|
||||
### クライアントトークンの生成
|
||||
|
||||
```python
|
||||
token = conn.generate_client_token()
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## CaptureClient
|
||||
|
||||
クライアントはユーザーのマシン上で動作し、権限、チャネルの発見、ストリーミングを処理する。
|
||||
|
||||
```python
|
||||
from videodb.capture import CaptureClient
|
||||
|
||||
client = CaptureClient(client_token=token)
|
||||
```
|
||||
|
||||
### CaptureClientメソッド
|
||||
|
||||
| メソッド | 戻り値 | 説明 |
|
||||
|--------|---------|-------------|
|
||||
| `await client.request_permission(type)` | `None` | デバイスの権限をリクエストする(`"microphone"`、`"screen_capture"`) |
|
||||
| `await client.list_channels()` | `Channels` | 利用可能なオーディオ/ビデオチャネルを発見する |
|
||||
| `await client.start_capture_session(capture_session_id, channels, primary_video_channel_id)` | `None` | 選択したチャネルのストリーミングを開始する |
|
||||
| `await client.stop_capture()` | `None` | キャプチャセッションをグレースフルに停止する |
|
||||
| `await client.shutdown()` | `None` | クライアントリソースをクリーンアップする |
|
||||
|
||||
### 権限のリクエスト
|
||||
|
||||
```python
|
||||
await client.request_permission("microphone")
|
||||
await client.request_permission("screen_capture")
|
||||
```
|
||||
|
||||
### セッションの開始
|
||||
|
||||
```python
|
||||
selected_channels = [c for c in [mic, display, system_audio] if c]
|
||||
await client.start_capture_session(
|
||||
capture_session_id=session.id,
|
||||
channels=selected_channels,
|
||||
primary_video_channel_id=display.id if display else None,
|
||||
)
|
||||
```
|
||||
|
||||
### セッションの停止
|
||||
|
||||
```python
|
||||
await client.stop_capture()
|
||||
await client.shutdown()
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## チャネル
|
||||
|
||||
`client.list_channels()` によって返される。利用可能なデバイスをタイプ別にグループ化する。
|
||||
|
||||
```python
|
||||
channels = await client.list_channels()
|
||||
for ch in channels.all():
|
||||
print(f" {ch.id} ({ch.type}): {ch.name}")
|
||||
|
||||
mic = channels.mics.default
|
||||
display = channels.displays.default
|
||||
system_audio = channels.system_audio.default
|
||||
```
|
||||
|
||||
### チャネルグループ
|
||||
|
||||
| 属性 | 型 | 説明 |
|
||||
|----------|------|-------------|
|
||||
| `channels.mics` | `ChannelGroup` | 利用可能なマイク |
|
||||
| `channels.displays` | `ChannelGroup` | 利用可能な画面ディスプレイ |
|
||||
| `channels.system_audio` | `ChannelGroup` | 利用可能なシステムオーディオソース |
|
||||
|
||||
### ChannelGroupメソッドと属性
|
||||
|
||||
| メンバー | 型 | 説明 |
|
||||
|--------|------|-------------|
|
||||
| `group.default` | `Channel` | グループのデフォルトチャネル(または `None`) |
|
||||
| `group.all()` | `list[Channel]` | グループのすべてのチャネル |
|
||||
|
||||
### チャネル属性
|
||||
|
||||
| 属性 | 型 | 説明 |
|
||||
|----------|------|-------------|
|
||||
| `ch.id` | `str` | 一意のチャネルID |
|
||||
| `ch.type` | `str` | チャネルタイプ(`"mic"`、`"display"`、`"system_audio"`) |
|
||||
| `ch.name` | `str` | 人間が読めるチャネル名 |
|
||||
| `ch.store` | `bool` | 録画を永続化するかどうか(保存するには `True` に設定) |
|
||||
|
||||
`store = True` がない場合、ストリームはリアルタイムで処理されるが保存されない。
|
||||
|
||||
***
|
||||
|
||||
## RTStreamとAIパイプライン
|
||||
|
||||
セッションがアクティブになったら、`session.get_rtstream()` を使用してRTStreamオブジェクトを取得する。
|
||||
|
||||
RTStreamメソッド(インデックス作成、転写、アラート、バッチ設定)については [rtstream-reference.md](rtstream-reference.md) を参照。
|
||||
|
||||
***
|
||||
|
||||
## セッションライフサイクル
|
||||
|
||||
```
|
||||
create_capture_session()
|
||||
│
|
||||
v
|
||||
┌───────────────┐
|
||||
│ created │
|
||||
└───────┬───────┘
|
||||
│ client.start_capture_session()
|
||||
v
|
||||
┌───────────────┐ WebSocket: capture_session.starting
|
||||
│ starting │ ──> Capture channels connect
|
||||
└───────┬───────┘
|
||||
│
|
||||
v
|
||||
┌───────────────┐ WebSocket: capture_session.active
|
||||
│ active │ ──> Start AI pipelines
|
||||
└───────┬──────────────┐
|
||||
│ │
|
||||
│ v
|
||||
│ ┌───────────────┐ WebSocket: capture_session.failed
|
||||
│ │ failed │ ──> Inspect error payload and retry setup
|
||||
│ └───────────────┘
|
||||
│ unrecoverable capture error
|
||||
│
|
||||
│ client.stop_capture()
|
||||
v
|
||||
┌───────────────┐ WebSocket: capture_session.stopping
|
||||
│ stopping │ ──> Finalize streams
|
||||
└───────┬───────┘
|
||||
│
|
||||
v
|
||||
┌───────────────┐ WebSocket: capture_session.stopped
|
||||
│ stopped │ ──> All streams finalized
|
||||
└───────┬───────┘
|
||||
│ (if store=True)
|
||||
v
|
||||
┌───────────────┐ WebSocket: capture_session.exported
|
||||
│ exported │ ──> Access video_id, stream_url, player_url
|
||||
└───────────────┘
|
||||
```
|
||||
104
docs/ja-JP/skills/videodb/reference/capture.md
Normal file
104
docs/ja-JP/skills/videodb/reference/capture.md
Normal file
@@ -0,0 +1,104 @@
|
||||
# キャプチャガイド
|
||||
|
||||
## 概要
|
||||
|
||||
VideoDB CaptureはAI処理機能を備えたリアルタイムの画面とオーディオの録画をサポートする。デスクトップキャプチャは現在**macOS**のみサポートされている。
|
||||
|
||||
コードレベルの詳細(SDKメソッド、イベント構造、AIパイプライン)については [capture-reference.md](capture-reference.md) を参照。
|
||||
|
||||
## クイックスタート
|
||||
|
||||
1. **WebSocketリスナーを起動する**:`python scripts/ws_listener.py --clear &`
|
||||
2. **キャプチャコードを実行する**(以下の完全なキャプチャワークフローを参照)
|
||||
3. **イベントの書き込み先**:`/tmp/videodb_events.jsonl`
|
||||
|
||||
***
|
||||
|
||||
## 完全なキャプチャワークフロー
|
||||
|
||||
WebhookやポーリングLiveEventは不要。WebSocketがセッションライフサイクルイベントを含むすべてのイベントを配信する。
|
||||
|
||||
> **重要な注意事項:** `CaptureClient` はキャプチャ全体を通じて実行し続ける必要がある。ローカルレコーダーバイナリを実行し、画面/オーディオデータをVideoDBにストリーミングする。`CaptureClient` を作成したPythonプロセスが終了すると、レコーダーバイナリが終了し、キャプチャが静かに停止する。常にキャプチャコードを**長期実行バックグラウンドプロセス**として実行し(例:`nohup python capture_script.py &`)、明示的に停止するまで生き続けるようにシグナル処理(`asyncio.Event` + `SIGINT`/`SIGTERM`)を使用すること。
|
||||
|
||||
1. バックグラウンドで**WebSocketリスナーを起動する**。古いイベントをクリアするために `--clear` フラグを使用する。WebSocket IDファイルが作成されるまで待つ。
|
||||
|
||||
2. **WebSocket IDを読み取る**。このIDはキャプチャセッションとAIパイプラインに必要。
|
||||
|
||||
3. **キャプチャセッションを作成する**。デスクトップクライアント用のクライアントトークンを生成する。
|
||||
|
||||
4. トークンを使用して**CaptureClientを初期化する**。マイクと画面キャプチャの権限をリクエストする。
|
||||
|
||||
5. **チャネルをリストアップして選択する**(マイク、ディスプレイ、システムオーディオ)。ビデオとして永続化したいチャネルに `store = True` を設定する。
|
||||
|
||||
6. 選択したチャネルで**セッションを開始する**。
|
||||
|
||||
7. `capture_session.active` が見えるまでイベントを読み取ることで**セッションがアクティブになるまで待つ**。このイベントには `rtstreams` 配列が含まれる。セッション情報(セッションID、RTStream ID)をファイルに保存する(例:`/tmp/videodb_capture_info.json`)。他のスクリプトがそれを読み取れるようにする。
|
||||
|
||||
8. **プロセスを生かし続ける**。明示的に停止されるまでプロセスをブロックするために、`SIGINT`/`SIGTERM` のシグナルハンドラーで `asyncio.Event` を使用する。後で `kill $(cat /tmp/videodb_capture_pid)` でプロセスを停止できるようにPIDファイルを書く(例:`/tmp/videodb_capture_pid`)。PIDファイルは実行のたびに上書きして、再実行時に常に正しいPIDを持つようにする。
|
||||
|
||||
9. 各RTStreamの音声インデックスとビジュアルインデックスを作成する**AIパイプラインを起動する**(別のコマンド/スクリプトで)。保存されたセッション情報ファイルからRTStream IDを読み取る。
|
||||
|
||||
10. ユースケースに応じてリアルタイムイベントを読み取る**カスタムイベント処理ロジックを書く**(別のコマンド/スクリプトで)。例:
|
||||
* `visual_index` が「Slack」を言及したときにSlackアクティビティをログに記録する
|
||||
* `audio_index` イベントが到着したときに議論をサマリーする
|
||||
* `transcript` に特定のキーワードが現れたときにアラートをトリガーする
|
||||
* 画面の説明からアプリの使用状況を追跡する
|
||||
|
||||
11. **キャプチャを停止する** - 完了したら、キャプチャプロセスにSIGTERMを送信する。シグナルハンドラーで `client.stop_capture()` と `client.shutdown()` を呼び出すべき。
|
||||
|
||||
12. **エクスポートを待つ** - `capture_session.exported` が見えるまでイベントを読み取る。このイベントには `exported_video_id`、`stream_url`、`player_url` が含まれる。キャプチャを停止した後、これには数秒かかる場合がある。
|
||||
|
||||
13. **WebSocketリスナーを停止する** - エクスポートイベントを受信したら、`kill $(cat /tmp/videodb_ws_pid)` でクリーンに終了させる。
|
||||
|
||||
***
|
||||
|
||||
## シャットダウンシーケンス
|
||||
|
||||
すべてのイベントがキャプチャされることを確認するために、適切なシャットダウンシーケンスが重要:
|
||||
|
||||
1. **キャプチャセッションを停止する** — `client.stop_capture()` 次に `client.shutdown()`
|
||||
2. **エクスポートイベントを待つ** — `capture_session.exported` を `/tmp/videodb_events.jsonl` でポーリングする
|
||||
3. **WebSocketリスナーを停止する** — `kill $(cat /tmp/videodb_ws_pid)`
|
||||
|
||||
エクスポートイベントを受信する前にWebSocketリスナーを**停止しないこと**。そうしないと最終的なビデオURLを受け取れなくなる。
|
||||
|
||||
***
|
||||
|
||||
## スクリプト
|
||||
|
||||
| スクリプト | 説明 |
|
||||
|--------|-------------|
|
||||
| `scripts/ws_listener.py` | WebSocketイベントリスナー(JSONLにダンプ) |
|
||||
|
||||
### ws\_listener.pyの使用方法
|
||||
|
||||
```bash
|
||||
# Start listener in background (append to existing events)
|
||||
python scripts/ws_listener.py &
|
||||
|
||||
# Start listener with clear (new session, clears old events)
|
||||
python scripts/ws_listener.py --clear &
|
||||
|
||||
# Custom output directory
|
||||
python scripts/ws_listener.py --clear /path/to/events &
|
||||
|
||||
# Stop the listener
|
||||
kill $(cat /tmp/videodb_ws_pid)
|
||||
```
|
||||
|
||||
**オプション:**
|
||||
|
||||
* `--clear`:起動前にイベントファイルをクリアする。新しいキャプチャセッションを開始するときに使用する。
|
||||
|
||||
**出力ファイル:**
|
||||
|
||||
* `videodb_events.jsonl` - すべてのWebSocketイベント
|
||||
* `videodb_ws_id` - WebSocket接続ID(`ws_connection_id` パラメータに使用)
|
||||
* `videodb_ws_pid` - プロセスID(リスナーの停止に使用)
|
||||
|
||||
**機能:**
|
||||
|
||||
* 接続が切れた場合の指数バックオフによる自動再接続
|
||||
* SIGINT/SIGTERMでのグレースフルシャットダウン
|
||||
* プロセス管理のためのPIDファイル
|
||||
* 接続状態のログ記録
|
||||
443
docs/ja-JP/skills/videodb/reference/editor.md
Normal file
443
docs/ja-JP/skills/videodb/reference/editor.md
Normal file
@@ -0,0 +1,443 @@
|
||||
# タイムライン編集ガイド
|
||||
|
||||
VideoDBは、複数のクリップからビデオを合成し、テキストや画像のオーバーレイを追加し、オーディオトラックをミックスし、クリップをトリミングするための非破壊的なタイムラインエディターを提供する——すべてサーバーサイドで、再エンコードやローカルツールは不要。トリミング、クリップのマージ、ビデオへのオーディオ/音楽のオーバーレイ、字幕の追加、テキストや画像のオーバーレイに使用できる。
|
||||
|
||||
## 前提条件
|
||||
|
||||
ビデオ、オーディオ、画像は、タイムラインアセットとして使用するために**コレクションにアップロードされている必要がある**。字幕オーバーレイには、ビデオも**音声単語のインデックスが作成されている必要がある**。
|
||||
|
||||
## コアコンセプト
|
||||
|
||||
### タイムライン
|
||||
|
||||
`Timeline` は仮想合成レイヤーである。アセットはタイムラインに**インライン**(メイントラックに順番に配置)または**オーバーレイ**(特定のタイムスタンプにレイヤーとして配置)として配置できる。元のメディアは変更されない;最終ストリームはオンデマンドでコンパイルされる。
|
||||
|
||||
```python
|
||||
from videodb.timeline import Timeline
|
||||
|
||||
timeline = Timeline(conn)
|
||||
```
|
||||
|
||||
### アセット
|
||||
|
||||
タイムライン上の各要素は**アセット**である。VideoDBは5種類のアセットタイプを提供する:
|
||||
|
||||
| アセット | インポート | 主な用途 |
|
||||
|-------|--------|-------------|
|
||||
| `VideoAsset` | `from videodb.asset import VideoAsset` | ビデオクリップ(トリミング、順序付け) |
|
||||
| `AudioAsset` | `from videodb.asset import AudioAsset` | 音楽、効果音、ナレーション |
|
||||
| `ImageAsset` | `from videodb.asset import ImageAsset` | ロゴ、サムネイル、オーバーレイ |
|
||||
| `TextAsset` | `from videodb.asset import TextAsset, TextStyle` | タイトル、字幕、ローワーサード |
|
||||
| `CaptionAsset` | `from videodb.editor import CaptionAsset` | 自動レンダリング字幕(エディターAPI) |
|
||||
|
||||
## タイムラインの構築
|
||||
|
||||
### ビデオクリップをインラインで追加する
|
||||
|
||||
インラインアセットはメインビデオトラックに順番に再生される。`add_inline` メソッドは `VideoAsset` のみを受け入れる:
|
||||
|
||||
```python
|
||||
from videodb.asset import VideoAsset
|
||||
|
||||
video_a = coll.get_video(video_id_a)
|
||||
video_b = coll.get_video(video_id_b)
|
||||
|
||||
timeline = Timeline(conn)
|
||||
timeline.add_inline(VideoAsset(asset_id=video_a.id))
|
||||
timeline.add_inline(VideoAsset(asset_id=video_b.id))
|
||||
|
||||
stream_url = timeline.generate_stream()
|
||||
```
|
||||
|
||||
### トリミング / サブクリップ
|
||||
|
||||
`VideoAsset` の `start` と `end` を使用して一部を抽出する:
|
||||
|
||||
```python
|
||||
# Take only seconds 10–30 from the source video
|
||||
clip = VideoAsset(asset_id=video.id, start=10, end=30)
|
||||
timeline.add_inline(clip)
|
||||
```
|
||||
|
||||
### VideoAssetのパラメータ
|
||||
|
||||
| パラメータ | 型 | デフォルト | 説明 |
|
||||
|-----------|------|---------|-------------|
|
||||
| `asset_id` | `str` | 必須 | ビデオメディアID |
|
||||
| `start` | `float` | `0` | トリミング開始時間(秒) |
|
||||
| `end` | `float\|None` | `None` | トリミング終了時間(`None` = 完全なビデオ) |
|
||||
|
||||
> **警告:** SDKは負のタイムスタンプを検証しない。`start=-5` を渡すと静かに受け入れられるが、破損したまたは予期しない出力を生成する。`VideoAsset` を作成する前に常に `start >= 0`、`start < end`、`end <= video.length` を確認すること。
|
||||
|
||||
## テキストオーバーレイ
|
||||
|
||||
タイムラインの任意の点にタイトル、ローワーサード、またはアノテーションを追加する:
|
||||
|
||||
```python
|
||||
from videodb.asset import TextAsset, TextStyle
|
||||
|
||||
title = TextAsset(
|
||||
text="Welcome to the Demo",
|
||||
duration=5,
|
||||
style=TextStyle(
|
||||
fontsize=36,
|
||||
fontcolor="white",
|
||||
boxcolor="black",
|
||||
alpha=0.8,
|
||||
font="Sans",
|
||||
),
|
||||
)
|
||||
|
||||
# Overlay the title at the very start (t=0)
|
||||
timeline.add_overlay(0, title)
|
||||
```
|
||||
|
||||
### TextStyleのパラメータ
|
||||
|
||||
| パラメータ | 型 | デフォルト | 説明 |
|
||||
|-----------|------|---------|-------------|
|
||||
| `fontsize` | `int` | `24` | フォントサイズ(ピクセル) |
|
||||
| `fontcolor` | `str` | `"black"` | CSSカラー名または16進数値 |
|
||||
| `fontcolor_expr` | `str` | `""` | 動的フォントカラー式 |
|
||||
| `alpha` | `float` | `1.0` | テキストの不透明度(0.0〜1.0) |
|
||||
| `font` | `str` | `"Sans"` | フォントファミリー |
|
||||
| `box` | `bool` | `True` | 背景ボックスを有効にする |
|
||||
| `boxcolor` | `str` | `"white"` | 背景ボックスカラー |
|
||||
| `boxborderw` | `str` | `"10"` | ボックスの境界線幅 |
|
||||
| `boxw` | `int` | `0` | ボックス幅のオーバーライド |
|
||||
| `boxh` | `int` | `0` | ボックス高さのオーバーライド |
|
||||
| `line_spacing` | `int` | `0` | 行間隔 |
|
||||
| `text_align` | `str` | `"T"` | ボックス内のテキスト整列 |
|
||||
| `y_align` | `str` | `"text"` | 垂直整列の基準 |
|
||||
| `borderw` | `int` | `0` | テキスト境界線幅 |
|
||||
| `bordercolor` | `str` | `"black"` | テキスト境界線カラー |
|
||||
| `expansion` | `str` | `"normal"` | テキスト展開モード |
|
||||
| `basetime` | `int` | `0` | 時間ベースの式の基準時間 |
|
||||
| `fix_bounds` | `bool` | `False` | テキスト境界を固定する |
|
||||
| `text_shaping` | `bool` | `True` | テキストシェーピングを有効にする |
|
||||
| `shadowcolor` | `str` | `"black"` | シャドウカラー |
|
||||
| `shadowx` | `int` | `0` | シャドウXオフセット |
|
||||
| `shadowy` | `int` | `0` | シャドウYオフセット |
|
||||
| `tabsize` | `int` | `4` | タブサイズ(スペース数) |
|
||||
| `x` | `str` | `"(main_w-text_w)/2"` | 水平位置の式 |
|
||||
| `y` | `str` | `"(main_h-text_h)/2"` | 垂直位置の式 |
|
||||
|
||||
## オーディオオーバーレイ
|
||||
|
||||
バックグラウンドミュージック、効果音、またはナレーションをメインビデオトラックの上にオーバーレイする:
|
||||
|
||||
```python
|
||||
from videodb.asset import AudioAsset
|
||||
|
||||
music = coll.get_audio(music_id)
|
||||
|
||||
audio_layer = AudioAsset(
|
||||
asset_id=music.id,
|
||||
disable_other_tracks=False,
|
||||
fade_in_duration=2,
|
||||
fade_out_duration=2,
|
||||
)
|
||||
|
||||
# Start the music at t=0, overlaid on the video track
|
||||
timeline.add_overlay(0, audio_layer)
|
||||
```
|
||||
|
||||
### AudioAssetのパラメータ
|
||||
|
||||
| パラメータ | 型 | デフォルト | 説明 |
|
||||
|-----------|------|---------|-------------|
|
||||
| `asset_id` | `str` | 必須 | オーディオメディアID |
|
||||
| `start` | `float` | `0` | トリミング開始時間(秒) |
|
||||
| `end` | `float\|None` | `None` | トリミング終了時間(`None` = 完全なオーディオ) |
|
||||
| `disable_other_tracks` | `bool` | `True` | Trueの場合、他のオーディオトラックをミュートする |
|
||||
| `fade_in_duration` | `float` | `0` | フェードイン秒数(最大5) |
|
||||
| `fade_out_duration` | `float` | `0` | フェードアウト秒数(最大5) |
|
||||
|
||||
## 画像オーバーレイ
|
||||
|
||||
ロゴ、ウォーターマーク、または生成された画像をオーバーレイとして追加する:
|
||||
|
||||
```python
|
||||
from videodb.asset import ImageAsset
|
||||
|
||||
logo = coll.get_image(logo_id)
|
||||
|
||||
logo_overlay = ImageAsset(
|
||||
asset_id=logo.id,
|
||||
duration=10,
|
||||
width=120,
|
||||
height=60,
|
||||
x=20,
|
||||
y=20,
|
||||
)
|
||||
|
||||
timeline.add_overlay(0, logo_overlay)
|
||||
```
|
||||
|
||||
### ImageAssetのパラメータ
|
||||
|
||||
| パラメータ | 型 | デフォルト | 説明 |
|
||||
|-----------|------|---------|-------------|
|
||||
| `asset_id` | `str` | 必須 | 画像メディアID |
|
||||
| `width` | `int\|str` | `100` | 表示幅 |
|
||||
| `height` | `int\|str` | `100` | 表示高さ |
|
||||
| `x` | `int` | `80` | 水平位置(左からのピクセル) |
|
||||
| `y` | `int` | `20` | 垂直位置(上からのピクセル) |
|
||||
| `duration` | `float\|None` | `None` | 表示時間(秒) |
|
||||
|
||||
## 字幕オーバーレイ
|
||||
|
||||
ビデオに字幕を追加する方法は2つある。
|
||||
|
||||
### 方法1:字幕ワークフロー(最もシンプル)
|
||||
|
||||
`video.add_subtitle()` を使用してビデオストリームに字幕を直接バーンインする。これは内部で `videodb.timeline.Timeline` を使用する:
|
||||
|
||||
```python
|
||||
from videodb import SubtitleStyle
|
||||
|
||||
# Video must have spoken words indexed first (force=True skips if already done)
|
||||
video.index_spoken_words(force=True)
|
||||
|
||||
# Add subtitles with default styling
|
||||
stream_url = video.add_subtitle()
|
||||
|
||||
# Or customise the subtitle style
|
||||
stream_url = video.add_subtitle(style=SubtitleStyle(
|
||||
font_name="Arial",
|
||||
font_size=22,
|
||||
primary_colour="&H00FFFFFF",
|
||||
bold=True,
|
||||
))
|
||||
```
|
||||
|
||||
### 方法2:エディターAPI(高度)
|
||||
|
||||
エディターAPI(`videodb.editor`)は、`CaptionAsset`、`Clip`、`Track`、独自の `Timeline` を持つトラックベースの合成システムを提供する。これは上記で使用した `videodb.timeline.Timeline` とは独立したAPIである。
|
||||
|
||||
```python
|
||||
from videodb.editor import (
|
||||
CaptionAsset,
|
||||
Clip,
|
||||
Track,
|
||||
Timeline as EditorTimeline,
|
||||
FontStyling,
|
||||
BorderAndShadow,
|
||||
Positioning,
|
||||
CaptionAnimation,
|
||||
)
|
||||
|
||||
# Video must have spoken words indexed first (force=True skips if already done)
|
||||
video.index_spoken_words(force=True)
|
||||
|
||||
# Create a caption asset
|
||||
caption = CaptionAsset(
|
||||
src="auto",
|
||||
font=FontStyling(name="Clear Sans", size=30),
|
||||
primary_color="&H00FFFFFF",
|
||||
back_color="&H00000000",
|
||||
border=BorderAndShadow(outline=1),
|
||||
position=Positioning(margin_v=30),
|
||||
animation=CaptionAnimation.box_highlight,
|
||||
)
|
||||
|
||||
# Build an editor timeline with tracks and clips
|
||||
editor_tl = EditorTimeline(conn)
|
||||
track = Track()
|
||||
track.add_clip(start=0, clip=Clip(asset=caption, duration=video.length))
|
||||
editor_tl.add_track(track)
|
||||
stream_url = editor_tl.generate_stream()
|
||||
```
|
||||
|
||||
### CaptionAssetのパラメータ
|
||||
|
||||
| パラメータ | 型 | デフォルト | 説明 |
|
||||
|-----------|------|---------|-------------|
|
||||
| `src` | `str` | `"auto"` | 字幕ソース(`"auto"` またはbase64 ASS文字列) |
|
||||
| `font` | `FontStyling\|None` | `FontStyling()` | フォントスタイリング(名前、サイズ、太字、斜体など) |
|
||||
| `primary_color` | `str` | `"&H00FFFFFF"` | メインテキストカラー(ASSフォーマット) |
|
||||
| `secondary_color` | `str` | `"&H000000FF"` | サブテキストカラー(ASSフォーマット) |
|
||||
| `back_color` | `str` | `"&H00000000"` | 背景カラー(ASSフォーマット) |
|
||||
| `border` | `BorderAndShadow\|None` | `BorderAndShadow()` | 境界線とシャドウのスタイル |
|
||||
| `position` | `Positioning\|None` | `Positioning()` | 字幕の整列とマージン |
|
||||
| `animation` | `CaptionAnimation\|None` | `None` | アニメーション効果(例:`box_highlight`、`reveal`、`karaoke`) |
|
||||
|
||||
## コンパイルとストリーミング
|
||||
|
||||
タイムラインを組み立てたら、ストリーミング可能なURLにコンパイルする。ストリームはオンザフライで生成される——レンダリングの待ち時間はない。
|
||||
|
||||
```python
|
||||
stream_url = timeline.generate_stream()
|
||||
print(f"Stream: {stream_url}")
|
||||
```
|
||||
|
||||
追加のストリーミングオプション(セグメントストリーム、検索からストリーム、オーディオ再生)については [streaming.md](streaming.md) を参照。
|
||||
|
||||
## 完全なワークフロー例
|
||||
|
||||
### タイトルカード付きのハイライトリール
|
||||
|
||||
```python
|
||||
import videodb
|
||||
from videodb import SearchType
|
||||
from videodb.exceptions import InvalidRequestError
|
||||
from videodb.timeline import Timeline
|
||||
from videodb.asset import VideoAsset, TextAsset, TextStyle
|
||||
|
||||
conn = videodb.connect()
|
||||
coll = conn.get_collection()
|
||||
video = coll.get_video("your-video-id")
|
||||
|
||||
# 1. Search for key moments
|
||||
video.index_spoken_words(force=True)
|
||||
try:
|
||||
results = video.search("product announcement", search_type=SearchType.semantic)
|
||||
shots = results.get_shots()
|
||||
except InvalidRequestError as exc:
|
||||
if "No results found" in str(exc):
|
||||
shots = []
|
||||
else:
|
||||
raise
|
||||
|
||||
# 2. Build timeline
|
||||
timeline = Timeline(conn)
|
||||
|
||||
# Title card
|
||||
title = TextAsset(
|
||||
text="Product Launch Highlights",
|
||||
duration=4,
|
||||
style=TextStyle(fontsize=48, fontcolor="white", boxcolor="#1a1a2e", alpha=0.95),
|
||||
)
|
||||
timeline.add_overlay(0, title)
|
||||
|
||||
# Append each matching clip
|
||||
for shot in shots:
|
||||
asset = VideoAsset(asset_id=shot.video_id, start=shot.start, end=shot.end)
|
||||
timeline.add_inline(asset)
|
||||
|
||||
# 3. Generate stream
|
||||
stream_url = timeline.generate_stream()
|
||||
print(f"Highlight reel: {stream_url}")
|
||||
```
|
||||
|
||||
### バックグラウンドミュージック付きロゴオーバーレイ
|
||||
|
||||
```python
|
||||
import videodb
|
||||
from videodb.timeline import Timeline
|
||||
from videodb.asset import VideoAsset, AudioAsset, ImageAsset
|
||||
|
||||
conn = videodb.connect()
|
||||
coll = conn.get_collection()
|
||||
|
||||
main_video = coll.get_video(main_video_id)
|
||||
music = coll.get_audio(music_id)
|
||||
logo = coll.get_image(logo_id)
|
||||
|
||||
timeline = Timeline(conn)
|
||||
|
||||
# Main video track
|
||||
timeline.add_inline(VideoAsset(asset_id=main_video.id))
|
||||
|
||||
# Background music — disable_other_tracks=False to mix with video audio
|
||||
timeline.add_overlay(
|
||||
0,
|
||||
AudioAsset(asset_id=music.id, disable_other_tracks=False, fade_in_duration=3),
|
||||
)
|
||||
|
||||
# Logo in top-right corner for first 10 seconds
|
||||
timeline.add_overlay(
|
||||
0,
|
||||
ImageAsset(asset_id=logo.id, duration=10, x=1140, y=20, width=120, height=60),
|
||||
)
|
||||
|
||||
stream_url = timeline.generate_stream()
|
||||
print(f"Final video: {stream_url}")
|
||||
```
|
||||
|
||||
### 複数のビデオからのマルチクリップモンタージュ
|
||||
|
||||
```python
|
||||
import videodb
|
||||
from videodb.timeline import Timeline
|
||||
from videodb.asset import VideoAsset, TextAsset, TextStyle
|
||||
|
||||
conn = videodb.connect()
|
||||
coll = conn.get_collection()
|
||||
|
||||
clips = [
|
||||
{"video_id": "vid_001", "start": 5, "end": 15, "label": "Scene 1"},
|
||||
{"video_id": "vid_002", "start": 0, "end": 20, "label": "Scene 2"},
|
||||
{"video_id": "vid_003", "start": 30, "end": 45, "label": "Scene 3"},
|
||||
]
|
||||
|
||||
timeline = Timeline(conn)
|
||||
timeline_offset = 0.0
|
||||
|
||||
for clip in clips:
|
||||
# Add a label as an overlay on each clip
|
||||
label = TextAsset(
|
||||
text=clip["label"],
|
||||
duration=2,
|
||||
style=TextStyle(fontsize=32, fontcolor="white", boxcolor="#333333"),
|
||||
)
|
||||
timeline.add_inline(
|
||||
VideoAsset(asset_id=clip["video_id"], start=clip["start"], end=clip["end"])
|
||||
)
|
||||
timeline.add_overlay(timeline_offset, label)
|
||||
timeline_offset += clip["end"] - clip["start"]
|
||||
|
||||
stream_url = timeline.generate_stream()
|
||||
print(f"Montage: {stream_url}")
|
||||
```
|
||||
|
||||
## 2つのタイムラインAPI
|
||||
|
||||
VideoDBには2つの独立したタイムラインシステムがある。それらは**互換性がない**:
|
||||
|
||||
| | `videodb.timeline.Timeline` | `videodb.editor.Timeline`(エディターAPI) |
|
||||
|---|---|---|
|
||||
| **インポート** | `from videodb.timeline import Timeline` | `from videodb.editor import Timeline as EditorTimeline` |
|
||||
| **アセット** | `VideoAsset`、`AudioAsset`、`ImageAsset`、`TextAsset` | `CaptionAsset`、`Clip`、`Track` |
|
||||
| **メソッド** | `add_inline()`、`add_overlay()` | `add_track()` と `Track` / `Clip` の組み合わせ |
|
||||
| **最適な用途** | ビデオ合成、オーバーレイ、マルチクリップ編集 | アニメーション付き字幕/キャプションスタイリング |
|
||||
|
||||
一方のAPIのアセットをもう一方に混在させない。`CaptionAsset` はエディターAPIのみで機能する。`VideoAsset` / `AudioAsset` / `ImageAsset` / `TextAsset` は `videodb.timeline.Timeline` のみで機能する。
|
||||
|
||||
## 制限と制約
|
||||
|
||||
タイムラインエディターは**非破壊的な線形合成**向けに設計されている。以下の操作は**サポートされていない**:
|
||||
|
||||
### サポートされていない操作
|
||||
|
||||
| 制限 | 詳細 |
|
||||
|---|---|
|
||||
| **トランジションやエフェクトなし** | クリップ間のクロスフェード、ワイプ、ディゾルブ、トランジションはない。すべてのカットはハードカット。 |
|
||||
| **ビデオへのビデオオーバーレイなし(ピクチャーインピクチャー)** | `add_inline()` は `VideoAsset` のみを受け入れる。別のビデオストリームの上に1つのビデオストリームをオーバーレイすることはできない。画像オーバーレイは静的なピクチャーインピクチャーを近似できるが、ライブビデオではない。 |
|
||||
| **速度や再生制御なし** | スローモーション、早送り、逆再生、タイムリマッピングはない。`VideoAsset` には `speed` パラメータがない。 |
|
||||
| **クロップ、ズーム、パンなし** | ビデオフレームの領域をクロップしたり、ズームエフェクトを適用したり、フレームでパンすることはできない。`video.reframe()` はアスペクト比変換のみ。 |
|
||||
| **ビデオフィルターやカラーグレーディングなし** | 輝度、コントラスト、彩度、色相、カラーコレクション調整はない。 |
|
||||
| **アニメーションテキストなし** | `TextAsset` はその全持続時間にわたって静的。フェードイン/アウト、移動、アニメーションはない。アニメーション字幕にはエディターAPIで `CaptionAsset` を使用する。 |
|
||||
| **混合テキストスタイルなし** | 単一の `TextAsset` は1つの `TextStyle` のみを持つ。単一のテキストブロック内で太字、斜体、カラーを混在させることはできない。 |
|
||||
| **ブランクまたは単色クリップなし** | 単色フレーム、ブラックスクリーン、スタンドアロンタイトルカードを作成することはできない。テキストと画像のオーバーレイは、インライントラックに基礎として `VideoAsset` が必要。 |
|
||||
| **オーディオ音量コントロールなし** | `AudioAsset` には `volume` パラメータがない。オーディオはフルボリュームか、`disable_other_tracks` でミュートかのどちらか。低音量でミックスすることはできない。 |
|
||||
| **キーフレームアニメーションなし** | 時間をかけてオーバーレイプロパティを変更することはできない(例:画像を位置Aから位置Bに移動)。 |
|
||||
|
||||
### 制約
|
||||
|
||||
| 制約 | 詳細 |
|
||||
|---|---|
|
||||
| **オーディオフェードは最大5秒** | `fade_in_duration` と `fade_out_duration` はそれぞれ最大5秒。 |
|
||||
| **オーバーレイの位置は絶対タイムライン基準** | オーバーレイはタイムライン開始からの絶対タイムスタンプを使用する。インラインクリップの再配置によってオーバーレイは移動しない。 |
|
||||
| **インライントラックはビデオのみ** | `add_inline()` は `VideoAsset` のみを受け入れる。オーディオ、画像、テキストは `add_overlay()` を使用する必要がある。 |
|
||||
| **オーバーレイはクリップにバインドされない** | オーバーレイは固定されたタイムラインタイムスタンプに配置される。オーバーレイを特定のインラインクリップに添付してそれと一緒に移動させることはできない。 |
|
||||
|
||||
## ヒント
|
||||
|
||||
* **非破壊的**:タイムラインはソースメディアを変更しない。同じアセットを使用して複数のタイムラインを作成できる。
|
||||
* **オーバーレイスタッキング**:複数のオーバーレイを同じタイムスタンプで開始できる。オーディオオーバーレイはミックスされる;画像/テキストオーバーレイは追加された順にレイヤー化される。
|
||||
* **インライントラックはVideoAssetのみ**:`add_inline()` は `VideoAsset` のみを受け入れる。`AudioAsset`、`ImageAsset`、`TextAsset` には `add_overlay()` を使用する。
|
||||
* **クリップ精度**:`VideoAsset` と `AudioAsset` の `start`/`end` は秒単位。
|
||||
* **ビデオオーディオのミュート**:音楽やナレーションをオーバーレイするときに元のビデオオーディオをミュートするために `AudioAsset` に `disable_other_tracks=True` を設定する。
|
||||
* **フェード制限**:`AudioAsset` の `fade_in_duration` と `fade_out_duration` は最大5秒。
|
||||
* **メディアの生成**:`coll.generate_music()`、`coll.generate_sound_effect()`、`coll.generate_voice()`、`coll.generate_image()` を使用してタイムラインアセットとしてすぐに使用できるメディアを作成する。
|
||||
331
docs/ja-JP/skills/videodb/reference/generative.md
Normal file
331
docs/ja-JP/skills/videodb/reference/generative.md
Normal file
@@ -0,0 +1,331 @@
|
||||
# 生成メディアガイド
|
||||
|
||||
VideoDBはAI駆動の画像、ビデオ、音楽、効果音、音声、テキストコンテンツ生成を提供する。すべての生成メソッドは**Collection**オブジェクト上にある。
|
||||
|
||||
## 前提条件
|
||||
|
||||
生成メソッドを呼び出す前に、接続とコレクションの参照が必要:
|
||||
|
||||
```python
|
||||
import videodb
|
||||
|
||||
conn = videodb.connect()
|
||||
coll = conn.get_collection()
|
||||
```
|
||||
|
||||
## 画像生成
|
||||
|
||||
テキストプロンプトから画像を生成する:
|
||||
|
||||
```python
|
||||
image = coll.generate_image(
|
||||
prompt="a futuristic cityscape at sunset with flying cars",
|
||||
aspect_ratio="16:9",
|
||||
)
|
||||
|
||||
# Access the generated image
|
||||
print(image.id)
|
||||
print(image.generate_url()) # returns a signed download URL
|
||||
```
|
||||
|
||||
### generate\_imageのパラメータ
|
||||
|
||||
| パラメータ | 型 | デフォルト | 説明 |
|
||||
|-----------|------|---------|-------------|
|
||||
| `prompt` | `str` | 必須 | 生成する画像のテキスト説明 |
|
||||
| `aspect_ratio` | `str` | `"1:1"` | アスペクト比:`"1:1"`, `"9:16"`, `"16:9"`, `"4:3"`, または `"3:4"` |
|
||||
| `callback_url` | `str\|None` | `None` | 非同期コールバックを受信するURL |
|
||||
|
||||
`.id`、`.name`、`.collection_id` を含む `Image` オブジェクトを返す。生成された画像の `.url` 属性は `None` になる可能性がある——信頼できる署名付きダウンロードURLを取得するには常に `image.generate_url()` を使用すること。
|
||||
|
||||
> **注意:** `Video` オブジェクト(`.generate_stream()` を使用)と異なり、`Image` オブジェクトは画像URLを取得するために `.generate_url()` を使用する。`.url` 属性は特定の画像タイプ(例:サムネイル)に対してのみ設定される。
|
||||
|
||||
## ビデオ生成
|
||||
|
||||
テキストプロンプトから短いビデオクリップを生成する:
|
||||
|
||||
```python
|
||||
video = coll.generate_video(
|
||||
prompt="a timelapse of a flower blooming in a garden",
|
||||
duration=5,
|
||||
)
|
||||
|
||||
stream_url = video.generate_stream()
|
||||
video.play()
|
||||
```
|
||||
|
||||
### generate\_videoのパラメータ
|
||||
|
||||
| パラメータ | 型 | デフォルト | 説明 |
|
||||
|-----------|------|---------|-------------|
|
||||
| `prompt` | `str` | 必須 | 生成するビデオのテキスト説明 |
|
||||
| `duration` | `int` | `5` | 長さ(秒)(整数値、5〜8でなければならない) |
|
||||
| `callback_url` | `str\|None` | `None` | 非同期コールバックを受信するURL |
|
||||
|
||||
`Video` オブジェクトを返す。生成されたビデオは自動的にコレクションに追加され、アップロードされたビデオと同様にタイムライン、検索、コンパイルで使用できる。
|
||||
|
||||
## オーディオ生成
|
||||
|
||||
VideoDBは異なるオーディオタイプのために3つの独立したメソッドを提供する。
|
||||
|
||||
### 音楽
|
||||
|
||||
テキスト説明からバックグラウンドミュージックを生成する:
|
||||
|
||||
```python
|
||||
music = coll.generate_music(
|
||||
prompt="upbeat electronic music with a driving beat, suitable for a tech demo",
|
||||
duration=30,
|
||||
)
|
||||
|
||||
print(music.id)
|
||||
```
|
||||
|
||||
| パラメータ | 型 | デフォルト | 説明 |
|
||||
|-----------|------|---------|-------------|
|
||||
| `prompt` | `str` | 必須 | 音楽のテキスト説明 |
|
||||
| `duration` | `int` | `5` | 長さ(秒) |
|
||||
| `callback_url` | `str\|None` | `None` | 非同期コールバックを受信するURL |
|
||||
|
||||
### 効果音
|
||||
|
||||
特定の効果音を生成する:
|
||||
|
||||
```python
|
||||
sfx = coll.generate_sound_effect(
|
||||
prompt="thunderstorm with heavy rain and distant thunder",
|
||||
duration=10,
|
||||
)
|
||||
```
|
||||
|
||||
| パラメータ | 型 | デフォルト | 説明 |
|
||||
|-----------|------|---------|-------------|
|
||||
| `prompt` | `str` | 必須 | 効果音のテキスト説明 |
|
||||
| `duration` | `int` | `2` | 長さ(秒) |
|
||||
| `config` | `dict` | `{}` | 追加設定 |
|
||||
| `callback_url` | `str\|None` | `None` | 非同期コールバックを受信するURL |
|
||||
|
||||
### 音声(テキスト読み上げ)
|
||||
|
||||
テキストから音声を生成する:
|
||||
|
||||
```python
|
||||
voice = coll.generate_voice(
|
||||
text="Welcome to our product demo. Today we'll walk through the key features.",
|
||||
voice_name="Default",
|
||||
)
|
||||
```
|
||||
|
||||
| パラメータ | 型 | デフォルト | 説明 |
|
||||
|-----------|------|---------|-------------|
|
||||
| `text` | `str` | 必須 | 音声に変換するテキスト |
|
||||
| `voice_name` | `str` | `"Default"` | 使用する音声 |
|
||||
| `config` | `dict` | `{}` | 追加設定 |
|
||||
| `callback_url` | `str\|None` | `None` | 非同期コールバックを受信するURL |
|
||||
|
||||
3つのオーディオメソッドはすべて `.id`、`.name`、`.length`、`.collection_id` を含む `Audio` オブジェクトを返す。
|
||||
|
||||
## テキスト生成(LLM統合)
|
||||
|
||||
`coll.generate_text()` を使用してLLM分析を実行する。これは**コレクションレベル**のメソッド——プロンプト文字列に任意のコンテキスト(トランスクリプト、説明)を直接渡す。
|
||||
|
||||
```python
|
||||
# Get transcript from a video first
|
||||
transcript_text = video.get_transcript_text()
|
||||
|
||||
# Generate analysis using collection LLM
|
||||
result = coll.generate_text(
|
||||
prompt=f"Summarize the key points discussed in this video:\n{transcript_text}",
|
||||
model_name="pro",
|
||||
)
|
||||
|
||||
print(result["output"])
|
||||
```
|
||||
|
||||
### generate\_textのパラメータ
|
||||
|
||||
| パラメータ | 型 | デフォルト | 説明 |
|
||||
|-----------|------|---------|-------------|
|
||||
| `prompt` | `str` | 必須 | LLMコンテキストを含むプロンプト |
|
||||
| `model_name` | `str` | `"basic"` | モデル層:`"basic"`、`"pro"`、または `"ultra"` |
|
||||
| `response_type` | `str` | `"text"` | レスポンスフォーマット:`"text"` または `"json"` |
|
||||
|
||||
`output` キーを持つ `dict` を返す。`response_type="text"` の場合、`output` は `str`。`response_type="json"` の場合、`output` は `dict`。
|
||||
|
||||
```python
|
||||
result = coll.generate_text(prompt="Summarize this", model_name="pro")
|
||||
print(result["output"]) # access the actual text/dict
|
||||
```
|
||||
|
||||
### LLMを使用したシーン分析
|
||||
|
||||
シーン抽出とテキスト生成を組み合わせる:
|
||||
|
||||
```python
|
||||
from videodb import SceneExtractionType
|
||||
|
||||
# First index scenes
|
||||
scenes = video.index_scenes(
|
||||
extraction_type=SceneExtractionType.time_based,
|
||||
extraction_config={"time": 10},
|
||||
prompt="Describe the visual content in this scene.",
|
||||
)
|
||||
|
||||
# Get transcript for spoken context
|
||||
transcript_text = video.get_transcript_text()
|
||||
scene_descriptions = []
|
||||
for scene in scenes:
|
||||
if isinstance(scene, dict):
|
||||
description = scene.get("description") or scene.get("summary")
|
||||
else:
|
||||
description = getattr(scene, "description", None) or getattr(scene, "summary", None)
|
||||
scene_descriptions.append(description or str(scene))
|
||||
|
||||
scenes_text = "\n".join(scene_descriptions)
|
||||
|
||||
# Analyze with collection LLM
|
||||
result = coll.generate_text(
|
||||
prompt=(
|
||||
f"Given this video transcript:\n{transcript_text}\n\n"
|
||||
f"And these visual scene descriptions:\n{scenes_text}\n\n"
|
||||
"Based on the spoken and visual content, describe the main topics covered."
|
||||
),
|
||||
model_name="pro",
|
||||
)
|
||||
print(result["output"])
|
||||
```
|
||||
|
||||
## 吹き替えと翻訳
|
||||
|
||||
### ビデオの吹き替え
|
||||
|
||||
コレクションメソッドを使用してビデオを別の言語に吹き替える:
|
||||
|
||||
```python
|
||||
dubbed_video = coll.dub_video(
|
||||
video_id=video.id,
|
||||
language_code="es", # Spanish
|
||||
)
|
||||
|
||||
dubbed_video.play()
|
||||
```
|
||||
|
||||
### dub\_videoのパラメータ
|
||||
|
||||
| パラメータ | 型 | デフォルト | 説明 |
|
||||
|-----------|------|---------|-------------|
|
||||
| `video_id` | `str` | 必須 | 吹き替えるビデオのID |
|
||||
| `language_code` | `str` | 必須 | ターゲット言語コード(例:`"es"`、`"fr"`、`"de"`) |
|
||||
| `callback_url` | `str\|None` | `None` | 非同期コールバックを受信するURL |
|
||||
|
||||
吹き替えられたコンテンツを含む `Video` オブジェクトを返す。
|
||||
|
||||
### トランスクリプトの翻訳
|
||||
|
||||
吹き替えなしでビデオのトランスクリプトを翻訳する:
|
||||
|
||||
```python
|
||||
translated = video.translate_transcript(
|
||||
language="Spanish",
|
||||
additional_notes="Use formal tone",
|
||||
)
|
||||
|
||||
for entry in translated:
|
||||
print(entry)
|
||||
```
|
||||
|
||||
**サポートされる言語**:`en`、`es`、`fr`、`de`、`it`、`pt`、`ja`、`ko`、`zh`、`hi`、`ar` など。
|
||||
|
||||
## 完全なワークフロー例
|
||||
|
||||
### ビデオのナレーション生成
|
||||
|
||||
```python
|
||||
import videodb
|
||||
|
||||
conn = videodb.connect()
|
||||
coll = conn.get_collection()
|
||||
video = coll.get_video("your-video-id")
|
||||
|
||||
# Get transcript
|
||||
transcript_text = video.get_transcript_text()
|
||||
|
||||
# Generate narration script using collection LLM
|
||||
result = coll.generate_text(
|
||||
prompt=(
|
||||
f"Write a professional narration script for this video content:\n"
|
||||
f"{transcript_text[:2000]}"
|
||||
),
|
||||
model_name="pro",
|
||||
)
|
||||
script = result["output"]
|
||||
|
||||
# Convert script to speech
|
||||
narration = coll.generate_voice(text=script)
|
||||
print(f"Narration audio: {narration.id}")
|
||||
```
|
||||
|
||||
### プロンプトからサムネイルを生成する
|
||||
|
||||
```python
|
||||
thumbnail = coll.generate_image(
|
||||
prompt="professional video thumbnail showing data analytics dashboard, modern design",
|
||||
aspect_ratio="16:9",
|
||||
)
|
||||
print(f"Thumbnail URL: {thumbnail.generate_url()}")
|
||||
```
|
||||
|
||||
### ビデオに生成された音楽を追加する
|
||||
|
||||
```python
|
||||
import videodb
|
||||
from videodb.timeline import Timeline
|
||||
from videodb.asset import VideoAsset, AudioAsset
|
||||
|
||||
conn = videodb.connect()
|
||||
coll = conn.get_collection()
|
||||
video = coll.get_video("your-video-id")
|
||||
|
||||
# Generate background music
|
||||
music = coll.generate_music(
|
||||
prompt="calm ambient background music for a tutorial video",
|
||||
duration=60,
|
||||
)
|
||||
|
||||
# Build timeline with video + music overlay
|
||||
timeline = Timeline(conn)
|
||||
timeline.add_inline(VideoAsset(asset_id=video.id))
|
||||
timeline.add_overlay(0, AudioAsset(asset_id=music.id, disable_other_tracks=False))
|
||||
|
||||
stream_url = timeline.generate_stream()
|
||||
print(f"Video with music: {stream_url}")
|
||||
```
|
||||
|
||||
### 構造化JSON出力
|
||||
|
||||
```python
|
||||
transcript_text = video.get_transcript_text()
|
||||
|
||||
result = coll.generate_text(
|
||||
prompt=(
|
||||
f"Given this transcript:\n{transcript_text}\n\n"
|
||||
"Return a JSON object with keys: summary, topics (array), action_items (array)."
|
||||
),
|
||||
model_name="pro",
|
||||
response_type="json",
|
||||
)
|
||||
|
||||
# result["output"] is a dict when response_type="json"
|
||||
print(result["output"]["summary"])
|
||||
print(result["output"]["topics"])
|
||||
```
|
||||
|
||||
## ヒント
|
||||
|
||||
* **生成されたメディアは永続的**:すべての生成されたコンテンツはコレクションに保存され、再利用できる。
|
||||
* **3つのオーディオメソッド**:バックグラウンドミュージックには `generate_music()`、効果音には `generate_sound_effect()`、テキスト読み上げには `generate_voice()` を使用する。統一された `generate_audio()` メソッドはない。
|
||||
* **テキスト生成はコレクションレベル**:`coll.generate_text()` はビデオコンテンツに自動的にアクセスしない。`video.get_transcript_text()` でトランスクリプトを取得し、プロンプトに渡す。
|
||||
* **モデル層**:`"basic"` が最速、`"pro"` がバランスの取れたオプション、`"ultra"` が最高品質。ほとんどの分析タスクには `"pro"` を使用する。
|
||||
* **生成タイプを組み合わせる**:オーバーレイ用に画像を生成し、バックグラウンド用に音楽を生成し、ナレーション用に音声を生成し、タイムラインを使用してそれらを組み合わせる([editor.md](editor.md) を参照)。
|
||||
* **プロンプトの品質が重要**:説明的で具体的なプロンプトはすべての生成タイプでより良い結果を生む。
|
||||
* **画像のアスペクト比**:`"1:1"`、`"9:16"`、`"16:9"`、`"4:3"`、または `"3:4"` から選択する。
|
||||
567
docs/ja-JP/skills/videodb/reference/rtstream-reference.md
Normal file
567
docs/ja-JP/skills/videodb/reference/rtstream-reference.md
Normal file
@@ -0,0 +1,567 @@
|
||||
# RTStreamリファレンス
|
||||
|
||||
RTStream操作のコードレベルの詳細。ワークフローガイドは [rtstream.md](rtstream.md) を参照。
|
||||
使用ガイダンスとフロー選択については、[../SKILL.md](../SKILL.md) から始めること。
|
||||
|
||||
[docs.videodb.io](https://docs.videodb.io/pages/ingest/live-streams/realtime-apis.md) に基づく。
|
||||
|
||||
***
|
||||
|
||||
## CollectionのRTStreamメソッド
|
||||
|
||||
`Collection` 上でRTStreamを管理するメソッド:
|
||||
|
||||
| メソッド | 戻り値 | 説明 |
|
||||
|--------|---------|-------------|
|
||||
| `coll.connect_rtstream(url, name, ...)` | `RTStream` | RTSP/RTMP URLから新しいRTStreamを作成する |
|
||||
| `coll.get_rtstream(id)` | `RTStream` | IDで既存のRTStreamを取得する |
|
||||
| `coll.list_rtstreams(limit, offset, status, name, ordering)` | `List[RTStream]` | コレクション内のすべてのRTStreamをリストする |
|
||||
| `coll.search(query, namespace="rtstream")` | `RTStreamSearchResult` | すべてのRTStreamで検索する |
|
||||
|
||||
### RTStreamへの接続
|
||||
|
||||
```python
|
||||
import videodb
|
||||
|
||||
conn = videodb.connect()
|
||||
coll = conn.get_collection()
|
||||
|
||||
rtstream = coll.connect_rtstream(
|
||||
url="rtmp://your-stream-server/live/stream-key",
|
||||
name="My Live Stream",
|
||||
media_types=["video"], # or ["audio", "video"]
|
||||
sample_rate=30, # optional
|
||||
store=True, # enable recording storage for export
|
||||
enable_transcript=True, # optional
|
||||
ws_connection_id=ws_id, # optional, for real-time events
|
||||
)
|
||||
```
|
||||
|
||||
### 既存のRTStreamを取得する
|
||||
|
||||
```python
|
||||
rtstream = coll.get_rtstream("rts-xxx")
|
||||
```
|
||||
|
||||
### RTStreamをリストする
|
||||
|
||||
```python
|
||||
rtstreams = coll.list_rtstreams(
|
||||
limit=10,
|
||||
offset=0,
|
||||
status="connected", # optional filter
|
||||
name="meeting", # optional filter
|
||||
ordering="-created_at",
|
||||
)
|
||||
|
||||
for rts in rtstreams:
|
||||
print(f"{rts.id}: {rts.name} - {rts.status}")
|
||||
```
|
||||
|
||||
### キャプチャセッションから取得する
|
||||
|
||||
キャプチャセッションがアクティブになったら、RTStreamオブジェクトを取得する:
|
||||
|
||||
```python
|
||||
session = conn.get_capture_session(session_id)
|
||||
|
||||
mics = session.get_rtstream("mic")
|
||||
displays = session.get_rtstream("screen")
|
||||
system_audios = session.get_rtstream("system_audio")
|
||||
```
|
||||
|
||||
または `capture_session.active` WebSocketイベントの `rtstreams` データを使用する:
|
||||
|
||||
```python
|
||||
for rts in rtstreams:
|
||||
rtstream = coll.get_rtstream(rts["rtstream_id"])
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## RTStreamメソッド
|
||||
|
||||
| メソッド | 戻り値 | 説明 |
|
||||
|--------|---------|-------------|
|
||||
| `rtstream.start()` | `None` | 取り込みを開始する |
|
||||
| `rtstream.stop()` | `None` | 取り込みを停止する |
|
||||
| `rtstream.generate_stream(start, end)` | `str` | 録画されたセグメントをストリーミングする(Unixタイムスタンプ) |
|
||||
| `rtstream.export(name=None)` | `RTStreamExportResult` | 永続的なビデオとしてエクスポートする |
|
||||
| `rtstream.index_visuals(prompt, ...)` | `RTStreamSceneIndex` | AI分析付きのビジュアルインデックスを作成する |
|
||||
| `rtstream.index_audio(prompt, ...)` | `RTStreamSceneIndex` | LLMサマリー付きのオーディオインデックスを作成する |
|
||||
| `rtstream.list_scene_indexes()` | `List[RTStreamSceneIndex]` | ストリーム上のすべてのシーンインデックスをリストする |
|
||||
| `rtstream.get_scene_index(index_id)` | `RTStreamSceneIndex` | 特定のシーンインデックスを取得する |
|
||||
| `rtstream.search(query, ...)` | `RTStreamSearchResult` | インデックス化されたコンテンツを検索する |
|
||||
| `rtstream.start_transcript(ws_connection_id, engine)` | `dict` | リアルタイム転写を開始する |
|
||||
| `rtstream.get_transcript(page, page_size, start, end, since)` | `dict` | 転写ページを取得する |
|
||||
| `rtstream.stop_transcript(engine)` | `dict` | 転写を停止する |
|
||||
|
||||
***
|
||||
|
||||
## 開始と停止
|
||||
|
||||
```python
|
||||
# Begin ingestion
|
||||
rtstream.start()
|
||||
|
||||
# ... stream is being recorded ...
|
||||
|
||||
# Stop ingestion
|
||||
rtstream.stop()
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## ストリームの生成
|
||||
|
||||
秒数オフセットではなくUnixタイムスタンプを使用して録画から再生ストリームを生成する:
|
||||
|
||||
```python
|
||||
import time
|
||||
|
||||
start_ts = time.time()
|
||||
rtstream.start()
|
||||
|
||||
# Let it record for a while...
|
||||
time.sleep(60)
|
||||
|
||||
end_ts = time.time()
|
||||
rtstream.stop()
|
||||
|
||||
# Generate a stream URL for the recorded segment
|
||||
stream_url = rtstream.generate_stream(start=start_ts, end=end_ts)
|
||||
print(f"Recorded stream: {stream_url}")
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## ビデオとしてエクスポートする
|
||||
|
||||
録画されたストリームをコレクション内の永続的なビデオとしてエクスポートする:
|
||||
|
||||
```python
|
||||
export_result = rtstream.export(name="Meeting Recording 2024-01-15")
|
||||
|
||||
print(f"Video ID: {export_result.video_id}")
|
||||
print(f"Stream URL: {export_result.stream_url}")
|
||||
print(f"Player URL: {export_result.player_url}")
|
||||
print(f"Duration: {export_result.duration}s")
|
||||
```
|
||||
|
||||
### RTStreamExportResult属性
|
||||
|
||||
| 属性 | 型 | 説明 |
|
||||
|----------|------|-------------|
|
||||
| `video_id` | `str` | エクスポートされたビデオのID |
|
||||
| `stream_url` | `str` | HLSストリームURL |
|
||||
| `player_url` | `str` | Webプレーヤー URL |
|
||||
| `name` | `str` | ビデオ名 |
|
||||
| `duration` | `float` | 長さ(秒) |
|
||||
|
||||
***
|
||||
|
||||
## AIパイプライン
|
||||
|
||||
AIパイプラインはライブストリームを処理し、WebSocket経由で結果を送信する。
|
||||
|
||||
### RTStream AIパイプラインメソッド
|
||||
|
||||
| メソッド | 戻り値 | 説明 |
|
||||
|--------|---------|-------------|
|
||||
| `rtstream.index_audio(prompt, batch_config, ...)` | `RTStreamSceneIndex` | LLMサマリー付きのオーディオインデックスを開始する |
|
||||
| `rtstream.index_visuals(prompt, batch_config, ...)` | `RTStreamSceneIndex` | 画面コンテンツのビジュアルインデックスを開始する |
|
||||
|
||||
### オーディオインデックス
|
||||
|
||||
一定間隔でオーディオコンテンツのLLMサマリーを生成する:
|
||||
|
||||
```python
|
||||
audio_index = rtstream.index_audio(
|
||||
prompt="Summarize what is being discussed",
|
||||
batch_config={"type": "word", "value": 50},
|
||||
model_name=None, # optional
|
||||
name="meeting_audio", # optional
|
||||
ws_connection_id=ws_id,
|
||||
)
|
||||
```
|
||||
|
||||
**オーディオのbatch\_configオプション:**
|
||||
|
||||
| タイプ | 値 | 説明 |
|
||||
|------|-------|-------------|
|
||||
| `"word"` | count | N単語ごとにセグメント化 |
|
||||
| `"sentence"` | count | N文ごとにセグメント化 |
|
||||
| `"time"` | seconds | N秒ごとにセグメント化 |
|
||||
|
||||
例:
|
||||
|
||||
```python
|
||||
{"type": "word", "value": 50} # every 50 words
|
||||
{"type": "sentence", "value": 5} # every 5 sentences
|
||||
{"type": "time", "value": 30} # every 30 seconds
|
||||
```
|
||||
|
||||
結果は `audio_index` WebSocketチャネル経由で届く。
|
||||
|
||||
### ビジュアルインデックス
|
||||
|
||||
ビジュアルコンテンツのAI説明を生成する:
|
||||
|
||||
```python
|
||||
scene_index = rtstream.index_visuals(
|
||||
prompt="Describe what is happening on screen",
|
||||
batch_config={"type": "time", "value": 2, "frame_count": 5},
|
||||
model_name="basic",
|
||||
name="screen_monitor", # optional
|
||||
ws_connection_id=ws_id,
|
||||
)
|
||||
```
|
||||
|
||||
**パラメータ:**
|
||||
|
||||
| パラメータ | 型 | 説明 |
|
||||
|-----------|------|-------------|
|
||||
| `prompt` | `str` | AIモデルへの指示(構造化JSON出力をサポート) |
|
||||
| `batch_config` | `dict` | フレームサンプリングを制御する(以下を参照) |
|
||||
| `model_name` | `str` | モデル層:`"mini"`、`"basic"`、`"pro"`、`"ultra"` |
|
||||
| `name` | `str` | インデックス名(オプション) |
|
||||
| `ws_connection_id` | `str` | 結果を受信するWebSocket接続ID |
|
||||
|
||||
**ビジュアルのbatch\_config:**
|
||||
|
||||
| キー | 型 | 説明 |
|
||||
|-----|------|-------------|
|
||||
| `type` | `str` | ビジュアルインデックスでは `"time"` のみサポート |
|
||||
| `value` | `int` | ウィンドウサイズ(秒) |
|
||||
| `frame_count` | `int` | 各ウィンドウで抽出するフレーム数 |
|
||||
|
||||
例:`{"type": "time", "value": 2, "frame_count": 5}` は2秒ごとに5フレームをサンプリングしてモデルに送信する。
|
||||
|
||||
**構造化JSON出力:**
|
||||
|
||||
構造化されたレスポンスを得るためにJSONフォーマットをリクエストするプロンプトを使用する:
|
||||
|
||||
```python
|
||||
scene_index = rtstream.index_visuals(
|
||||
prompt="""Analyze the screen and return a JSON object with:
|
||||
{
|
||||
"app_name": "name of the active application",
|
||||
"activity": "what the user is doing",
|
||||
"ui_elements": ["list of visible UI elements"],
|
||||
"contains_text": true/false,
|
||||
"dominant_colors": ["list of main colors"]
|
||||
}
|
||||
Return only valid JSON.""",
|
||||
batch_config={"type": "time", "value": 3, "frame_count": 3},
|
||||
model_name="pro",
|
||||
ws_connection_id=ws_id,
|
||||
)
|
||||
```
|
||||
|
||||
結果は `scene_index` WebSocketチャネル経由で届く。
|
||||
|
||||
***
|
||||
|
||||
## バッチ設定のサマリー
|
||||
|
||||
| インデックスタイプ | `type` オプション | `value` | 追加キー |
|
||||
|---------------|----------------|---------|------------|
|
||||
| **オーディオ** | `"word"`、`"sentence"`、`"time"` | words/sentences/seconds | - |
|
||||
| **ビジュアル** | `"time"` のみ | seconds | `frame_count` |
|
||||
|
||||
例:
|
||||
|
||||
```python
|
||||
# Audio: every 50 words
|
||||
{"type": "word", "value": 50}
|
||||
|
||||
# Audio: every 30 seconds
|
||||
{"type": "time", "value": 30}
|
||||
|
||||
# Visual: 5 frames every 2 seconds
|
||||
{"type": "time", "value": 2, "frame_count": 5}
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## 転写
|
||||
|
||||
WebSocket経由のリアルタイム転写:
|
||||
|
||||
```python
|
||||
# Start live transcription
|
||||
rtstream.start_transcript(
|
||||
ws_connection_id=ws_id,
|
||||
engine=None, # optional, defaults to "assemblyai"
|
||||
)
|
||||
|
||||
# Get transcript pages (with optional filters)
|
||||
transcript = rtstream.get_transcript(
|
||||
page=1,
|
||||
page_size=100,
|
||||
start=None, # optional: start timestamp filter
|
||||
end=None, # optional: end timestamp filter
|
||||
since=None, # optional: for polling, get transcripts after this timestamp
|
||||
engine=None,
|
||||
)
|
||||
|
||||
# Stop transcription
|
||||
rtstream.stop_transcript(engine=None)
|
||||
```
|
||||
|
||||
転写結果は `transcript` WebSocketチャネル経由で届く。
|
||||
|
||||
***
|
||||
|
||||
## RTStreamSceneIndex
|
||||
|
||||
`index_audio()` または `index_visuals()` を呼び出すと、メソッドは `RTStreamSceneIndex` オブジェクトを返す。このオブジェクトは実行中のインデックスを表し、シーンとアラートを管理するためのメソッドを提供する。
|
||||
|
||||
```python
|
||||
# index_visuals returns an RTStreamSceneIndex
|
||||
scene_index = rtstream.index_visuals(
|
||||
prompt="Describe what is on screen",
|
||||
ws_connection_id=ws_id,
|
||||
)
|
||||
|
||||
# index_audio also returns an RTStreamSceneIndex
|
||||
audio_index = rtstream.index_audio(
|
||||
prompt="Summarize the discussion",
|
||||
ws_connection_id=ws_id,
|
||||
)
|
||||
```
|
||||
|
||||
### RTStreamSceneIndex属性
|
||||
|
||||
| 属性 | 型 | 説明 |
|
||||
|----------|------|-------------|
|
||||
| `rtstream_index_id` | `str` | インデックスの一意ID |
|
||||
| `rtstream_id` | `str` | 親RTStreamのID |
|
||||
| `extraction_type` | `str` | 抽出タイプ(`time` または `transcript`) |
|
||||
| `extraction_config` | `dict` | 抽出設定 |
|
||||
| `prompt` | `str` | 分析に使用するプロンプト |
|
||||
| `name` | `str` | インデックス名 |
|
||||
| `status` | `str` | 状態(`connected`、`stopped`) |
|
||||
|
||||
### RTStreamSceneIndexメソッド
|
||||
|
||||
| メソッド | 戻り値 | 説明 |
|
||||
|--------|---------|-------------|
|
||||
| `index.get_scenes(start, end, page, page_size)` | `dict` | インデックス化されたシーンを取得する |
|
||||
| `index.start()` | `None` | インデックスを開始/再開する |
|
||||
| `index.stop()` | `None` | インデックスを停止する |
|
||||
| `index.create_alert(event_id, callback_url, ws_connection_id)` | `str` | イベント検出アラートを作成する |
|
||||
| `index.list_alerts()` | `list` | このインデックスのすべてのアラートをリストする |
|
||||
| `index.enable_alert(alert_id)` | `None` | アラートを有効にする |
|
||||
| `index.disable_alert(alert_id)` | `None` | アラートを無効にする |
|
||||
|
||||
### シーンの取得
|
||||
|
||||
インデックスからインデックス化されたシーンをポーリングする:
|
||||
|
||||
```python
|
||||
result = scene_index.get_scenes(
|
||||
start=None, # optional: start timestamp
|
||||
end=None, # optional: end timestamp
|
||||
page=1,
|
||||
page_size=100,
|
||||
)
|
||||
|
||||
for scene in result["scenes"]:
|
||||
print(f"[{scene['start']}-{scene['end']}] {scene['text']}")
|
||||
|
||||
if result["next_page"]:
|
||||
# fetch next page
|
||||
pass
|
||||
```
|
||||
|
||||
### シーンインデックスの管理
|
||||
|
||||
```python
|
||||
# List all indexes on the stream
|
||||
indexes = rtstream.list_scene_indexes()
|
||||
|
||||
# Get a specific index by ID
|
||||
scene_index = rtstream.get_scene_index(index_id)
|
||||
|
||||
# Stop an index
|
||||
scene_index.stop()
|
||||
|
||||
# Restart an index
|
||||
scene_index.start()
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## イベント
|
||||
|
||||
イベントは再利用可能な検出ルール。一度作成すれば、アラートを通じて任意のインデックスに添付できる。
|
||||
|
||||
### 接続イベントメソッド
|
||||
|
||||
| メソッド | 戻り値 | 説明 |
|
||||
|--------|---------|-------------|
|
||||
| `conn.create_event(event_prompt, label)` | `str` (event\_id) | 検出イベントを作成する |
|
||||
| `conn.list_events()` | `list` | すべてのイベントをリストする |
|
||||
|
||||
### イベントの作成
|
||||
|
||||
```python
|
||||
event_id = conn.create_event(
|
||||
event_prompt="User opened Slack application",
|
||||
label="slack_opened",
|
||||
)
|
||||
```
|
||||
|
||||
### イベントのリスト
|
||||
|
||||
```python
|
||||
events = conn.list_events()
|
||||
for event in events:
|
||||
print(f"{event['event_id']}: {event['label']}")
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## アラート
|
||||
|
||||
アラートはイベントをインデックスに接続してリアルタイム通知を実現する。AIがイベントの説明に一致するコンテンツを検出すると、アラートが送信される。
|
||||
|
||||
### アラートの作成
|
||||
|
||||
```python
|
||||
# Get the RTStreamSceneIndex from index_visuals
|
||||
scene_index = rtstream.index_visuals(
|
||||
prompt="Describe what application is open on screen",
|
||||
ws_connection_id=ws_id,
|
||||
)
|
||||
|
||||
# Create an alert on the index
|
||||
alert_id = scene_index.create_alert(
|
||||
event_id=event_id,
|
||||
callback_url="https://your-backend.com/alerts", # for webhook delivery
|
||||
ws_connection_id=ws_id, # for WebSocket delivery (optional)
|
||||
)
|
||||
```
|
||||
|
||||
**注意:** `callback_url` は必須。WebSocket配信のみを使用する場合は空文字列 `""` を渡す。
|
||||
|
||||
### アラートの管理
|
||||
|
||||
```python
|
||||
# List all alerts on an index
|
||||
alerts = scene_index.list_alerts()
|
||||
|
||||
# Enable/disable alerts
|
||||
scene_index.disable_alert(alert_id)
|
||||
scene_index.enable_alert(alert_id)
|
||||
```
|
||||
|
||||
### アラート配信
|
||||
|
||||
| 方法 | 遅延 | ユースケース |
|
||||
|--------|---------|----------|
|
||||
| WebSocket | リアルタイム | ダッシュボード、ライブUI |
|
||||
| Webhook | < 1秒 | サーバー間、自動化 |
|
||||
|
||||
### WebSocketアラートイベント
|
||||
|
||||
```json
|
||||
{
|
||||
"channel": "alert",
|
||||
"rtstream_id": "rts-xxx",
|
||||
"data": {
|
||||
"event_label": "slack_opened",
|
||||
"timestamp": 1710000012340,
|
||||
"text": "User opened Slack application"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Webhookペイロード
|
||||
|
||||
```json
|
||||
{
|
||||
"event_id": "event-xxx",
|
||||
"label": "slack_opened",
|
||||
"confidence": 0.95,
|
||||
"explanation": "User opened the Slack application",
|
||||
"timestamp": "2024-01-15T10:30:45Z",
|
||||
"start_time": 1234.5,
|
||||
"end_time": 1238.0,
|
||||
"stream_url": "https://stream.videodb.io/v3/...",
|
||||
"player_url": "https://console.videodb.io/player?url=..."
|
||||
}
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## WebSocket統合
|
||||
|
||||
すべてのリアルタイムAI結果はWebSocket経由で配信される。以下に `ws_connection_id` を渡す:
|
||||
|
||||
* `rtstream.start_transcript()`
|
||||
* `rtstream.index_audio()`
|
||||
* `rtstream.index_visuals()`
|
||||
* `scene_index.create_alert()`
|
||||
|
||||
### WebSocketチャネル
|
||||
|
||||
| チャネル | ソース | コンテンツ |
|
||||
|---------|--------|---------|
|
||||
| `transcript` | `start_transcript()` | リアルタイム音声テキスト変換 |
|
||||
| `scene_index` | `index_visuals()` | ビジュアル分析結果 |
|
||||
| `audio_index` | `index_audio()` | オーディオ分析結果 |
|
||||
| `alert` | `create_alert()` | アラート通知 |
|
||||
|
||||
WebSocketイベント構造とws\_listenerの使用については [capture-reference.md](capture-reference.md) を参照。
|
||||
|
||||
***
|
||||
|
||||
## 完全なワークフロー
|
||||
|
||||
```python
|
||||
import time
|
||||
import videodb
|
||||
from videodb.exceptions import InvalidRequestError
|
||||
|
||||
conn = videodb.connect()
|
||||
coll = conn.get_collection()
|
||||
|
||||
# 1. Connect and start recording
|
||||
rtstream = coll.connect_rtstream(
|
||||
url="rtmp://your-stream-server/live/stream-key",
|
||||
name="Weekly Standup",
|
||||
store=True,
|
||||
)
|
||||
rtstream.start()
|
||||
|
||||
# 2. Record for the duration of the meeting
|
||||
start_ts = time.time()
|
||||
time.sleep(1800) # 30 minutes
|
||||
end_ts = time.time()
|
||||
rtstream.stop()
|
||||
|
||||
# Generate an immediate playback URL for the captured window
|
||||
stream_url = rtstream.generate_stream(start=start_ts, end=end_ts)
|
||||
print(f"Recorded stream: {stream_url}")
|
||||
|
||||
# 3. Export to a permanent video
|
||||
export_result = rtstream.export(name="Weekly Standup Recording")
|
||||
print(f"Exported video: {export_result.video_id}")
|
||||
|
||||
# 4. Index the exported video for search
|
||||
video = coll.get_video(export_result.video_id)
|
||||
video.index_spoken_words(force=True)
|
||||
|
||||
# 5. Search for action items
|
||||
try:
|
||||
results = video.search("action items and next steps")
|
||||
stream_url = results.compile()
|
||||
print(f"Action items clip: {stream_url}")
|
||||
except InvalidRequestError as exc:
|
||||
if "No results found" in str(exc):
|
||||
print("No action items were detected in the recording.")
|
||||
else:
|
||||
raise
|
||||
```
|
||||
59
docs/ja-JP/skills/videodb/reference/rtstream.md
Normal file
59
docs/ja-JP/skills/videodb/reference/rtstream.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# RTStreamガイド
|
||||
|
||||
## 概要
|
||||
|
||||
RTStreamはライブビデオストリーム(RTSP/RTMP)とデスクトップキャプチャセッションのリアルタイム取り込みをサポートする。接続後は、ライブフィードのコンテンツを録画、インデックス作成、検索、エクスポートできる。
|
||||
|
||||
コードレベルの詳細(SDKメソッド、パラメータ、例)については [rtstream-reference.md](rtstream-reference.md) を参照。
|
||||
|
||||
## ユースケース
|
||||
|
||||
* **セキュリティと監視**:RTSPカメラに接続し、イベントを検出し、アラートをトリガーする
|
||||
* **ライブブロードキャスト**:RTMPストリームを取り込み、リアルタイムでインデックス化し、即時検索を実現する
|
||||
* **会議録画**:デスクトップ画面とオーディオをキャプチャし、リアルタイムで転写し、録画をエクスポートする
|
||||
* **イベント処理**:ライブビデオストリームを監視し、AI分析を実行し、検出されたコンテンツに応答する
|
||||
|
||||
## クイックスタート
|
||||
|
||||
1. **ライブストリームに接続する**(RTSP/RTMP URL)またはキャプチャセッションからRTStreamを取得する
|
||||
2. **取り込みを開始する**ことでライブコンテンツの録画を始める
|
||||
3. **AIパイプラインを起動する**ことでリアルタイムインデックス作成(オーディオ、ビジュアル、転写)を行う
|
||||
4. **WebSocketでイベントを監視する**ことでリアルタイムAI結果とアラートを取得する
|
||||
5. **完了したら取り込みを停止する**
|
||||
6. **ビデオとしてエクスポートする**ことで永続ストレージとさらなる処理を行う
|
||||
7. **録画を検索する**ことで特定のモーメントを見つける
|
||||
|
||||
## RTStreamソース
|
||||
|
||||
### RTSP/RTMPストリームから
|
||||
|
||||
ライブビデオフィードに直接接続する:
|
||||
|
||||
```python
|
||||
rtstream = coll.connect_rtstream(
|
||||
url="rtmp://your-stream-server/live/stream-key",
|
||||
name="My Live Stream",
|
||||
)
|
||||
```
|
||||
|
||||
### キャプチャセッションから
|
||||
|
||||
デスクトップキャプチャ(マイク、画面、システムオーディオ)からRTStreamを取得する:
|
||||
|
||||
```python
|
||||
session = conn.get_capture_session(session_id)
|
||||
|
||||
mics = session.get_rtstream("mic")
|
||||
displays = session.get_rtstream("screen")
|
||||
system_audios = session.get_rtstream("system_audio")
|
||||
```
|
||||
|
||||
キャプチャセッションのワークフローについては [capture.md](capture.md) を参照。
|
||||
|
||||
***
|
||||
|
||||
## スクリプト
|
||||
|
||||
| スクリプト | 説明 |
|
||||
|--------|-------------|
|
||||
| `scripts/ws_listener.py` | リアルタイムAI結果のためのWebSocketイベントリスナー |
|
||||
230
docs/ja-JP/skills/videodb/reference/search.md
Normal file
230
docs/ja-JP/skills/videodb/reference/search.md
Normal file
@@ -0,0 +1,230 @@
|
||||
# 検索とインデックスガイド
|
||||
|
||||
検索機能を使用すると、自然言語クエリ、正確なキーワード、またはビジュアルシーンの説明でビデオ内の特定のモーメントを見つけることができる。
|
||||
|
||||
## 前提条件
|
||||
|
||||
ビデオは検索の前に**インデックス化されている必要がある**。各インデックスタイプは各ビデオに対して1回だけ実行が必要。
|
||||
|
||||
## インデックス作成
|
||||
|
||||
### 音声単語インデックス
|
||||
|
||||
セマンティック検索とキーワード検索をサポートするためにビデオの転写音声コンテンツをインデックス化する:
|
||||
|
||||
```python
|
||||
video = coll.get_video(video_id)
|
||||
|
||||
# force=True makes indexing idempotent — skips if already indexed
|
||||
video.index_spoken_words(force=True)
|
||||
```
|
||||
|
||||
この操作はオーディオトラックを転写し、音声コンテンツ上に検索可能なインデックスを構築する。セマンティック検索とキーワード検索に必要。
|
||||
|
||||
**パラメータ:**
|
||||
|
||||
| パラメータ | 型 | デフォルト | 説明 |
|
||||
|-----------|------|---------|-------------|
|
||||
| `language_code` | `str\|None` | `None` | ビデオの言語コード |
|
||||
| `segmentation_type` | `SegmentationType` | `SegmentationType.sentence` | セグメンテーションタイプ(`sentence` または `llm`) |
|
||||
| `force` | `bool` | `False` | `True` に設定すると既にインデックス化済みをスキップする(「既に存在」エラーを回避) |
|
||||
| `callback_url` | `str\|None` | `None` | 非同期通知のWebhook URL |
|
||||
|
||||
### シーンインデックス
|
||||
|
||||
シーンのAI説明を生成することでビジュアルコンテンツをインデックス化する。音声単語インデックスと同様に、シーンインデックスが既に存在する場合はこの操作がエラーを発生させる。エラーメッセージから既存の `scene_index_id` を抽出する。
|
||||
|
||||
```python
|
||||
import re
|
||||
from videodb import SceneExtractionType
|
||||
|
||||
try:
|
||||
scene_index_id = video.index_scenes(
|
||||
extraction_type=SceneExtractionType.shot_based,
|
||||
prompt="Describe the visual content, objects, actions, and setting in this scene.",
|
||||
)
|
||||
except Exception as e:
|
||||
match = re.search(r"id\s+([a-f0-9]+)", str(e))
|
||||
if match:
|
||||
scene_index_id = match.group(1)
|
||||
else:
|
||||
raise
|
||||
```
|
||||
|
||||
**抽出タイプ:**
|
||||
|
||||
| タイプ | 説明 | 最適な用途 |
|
||||
|------|-------------|----------|
|
||||
| `SceneExtractionType.shot_based` | ビジュアルショット境界に基づいてセグメント化 | 汎用、アクションコンテンツ |
|
||||
| `SceneExtractionType.time_based` | 固定間隔でセグメント化 | 均一なサンプリング、長い静的コンテンツ |
|
||||
| `SceneExtractionType.transcript` | トランスクリプトセグメントに基づいてセグメント化 | 音声駆動のシーン境界 |
|
||||
|
||||
**`time_based` のパラメータ:**
|
||||
|
||||
```python
|
||||
video.index_scenes(
|
||||
extraction_type=SceneExtractionType.time_based,
|
||||
extraction_config={"time": 5, "select_frames": ["first", "last"]},
|
||||
prompt="Describe what is happening in this scene.",
|
||||
)
|
||||
```
|
||||
|
||||
## 検索タイプ
|
||||
|
||||
### セマンティック検索
|
||||
|
||||
自然言語クエリを使用して音声コンテンツを照合する:
|
||||
|
||||
```python
|
||||
from videodb import SearchType
|
||||
|
||||
results = video.search(
|
||||
query="explaining the benefits of machine learning",
|
||||
search_type=SearchType.semantic,
|
||||
)
|
||||
```
|
||||
|
||||
クエリとセマンティックに一致する音声コンテンツのランク付けされたクリップを返す。
|
||||
|
||||
### キーワード検索
|
||||
|
||||
転写された音声内で正確な用語照合を行う:
|
||||
|
||||
```python
|
||||
results = video.search(
|
||||
query="artificial intelligence",
|
||||
search_type=SearchType.keyword,
|
||||
)
|
||||
```
|
||||
|
||||
正確なキーワードまたはフレーズを含むクリップを返す。
|
||||
|
||||
### シーン検索
|
||||
|
||||
ビジュアルコンテンツクエリをインデックス化されたシーンの説明と照合する。事前に `index_scenes()` の呼び出しが必要。
|
||||
|
||||
`index_scenes()` は `scene_index_id` を返す。`video.search()` に渡して特定のシーンインデックスを対象にする(ビデオに複数のシーンインデックスがある場合に特に重要):
|
||||
|
||||
```python
|
||||
from videodb import SearchType, IndexType
|
||||
from videodb.exceptions import InvalidRequestError
|
||||
|
||||
# Search using semantic search against the scene index.
|
||||
# Use score_threshold to filter low-relevance noise (recommended: 0.3+).
|
||||
try:
|
||||
results = video.search(
|
||||
query="person writing on a whiteboard",
|
||||
search_type=SearchType.semantic,
|
||||
index_type=IndexType.scene,
|
||||
scene_index_id=scene_index_id,
|
||||
score_threshold=0.3,
|
||||
)
|
||||
shots = results.get_shots()
|
||||
except InvalidRequestError as e:
|
||||
if "No results found" in str(e):
|
||||
shots = []
|
||||
else:
|
||||
raise
|
||||
```
|
||||
|
||||
**重要な注意事項:**
|
||||
|
||||
* `SearchType.semantic` と `index_type=IndexType.scene` を組み合わせて使用する——これはすべてのプランで機能する最も信頼性の高い組み合わせ。
|
||||
* `SearchType.scene` は存在するが、すべてのプラン(例:無料プラン)で利用可能ではない可能性がある。`IndexType.scene` と `SearchType.semantic` を使用することを推奨する。
|
||||
* `scene_index_id` パラメータはオプション。省略すると、検索はビデオ上のすべてのシーンインデックスに対して実行される。特定のインデックスを対象にするためにこのパラメータを渡す。
|
||||
* 各ビデオに対して複数のシーンインデックスを作成し(異なるプロンプトや抽出タイプを使用して)、`scene_index_id` を使用して独立して検索できる。
|
||||
|
||||
### メタデータフィルター付きシーン検索
|
||||
|
||||
カスタムメタデータでシーンをインデックス化する場合、セマンティック検索とメタデータフィルターを組み合わせて使用できる:
|
||||
|
||||
```python
|
||||
from videodb import SearchType, IndexType
|
||||
|
||||
results = video.search(
|
||||
query="a skillful chasing scene",
|
||||
search_type=SearchType.semantic,
|
||||
index_type=IndexType.scene,
|
||||
scene_index_id=scene_index_id,
|
||||
filter=[{"camera_view": "road_ahead"}, {"action_type": "chasing"}],
|
||||
)
|
||||
```
|
||||
|
||||
カスタムメタデータインデックスとフィルター検索の完全な例については、[scene\_level\_metadata\_indexing 例](https://github.com/video-db/videodb-cookbook/blob/main/quickstart/scene_level_metadata_indexing.ipynb) を参照。
|
||||
|
||||
## 結果の処理
|
||||
|
||||
### クリップを取得する
|
||||
|
||||
個々の結果クリップにアクセスする:
|
||||
|
||||
```python
|
||||
results = video.search("your query")
|
||||
|
||||
for shot in results.get_shots():
|
||||
print(f"Video: {shot.video_id}")
|
||||
print(f"Start: {shot.start:.2f}s")
|
||||
print(f"End: {shot.end:.2f}s")
|
||||
print(f"Text: {shot.text}")
|
||||
print("---")
|
||||
```
|
||||
|
||||
### コンパイルされた結果を再生する
|
||||
|
||||
すべての一致するクリップを単一のコンパイルされたビデオとしてストリーミング再生する:
|
||||
|
||||
```python
|
||||
results = video.search("your query")
|
||||
stream_url = results.compile()
|
||||
results.play() # opens compiled stream in browser
|
||||
```
|
||||
|
||||
### クリップを抽出する
|
||||
|
||||
特定の結果クリップをダウンロードまたはストリーミングする:
|
||||
|
||||
```python
|
||||
for shot in results.get_shots():
|
||||
stream_url = shot.generate_stream()
|
||||
print(f"Clip: {stream_url}")
|
||||
```
|
||||
|
||||
## コレクション横断検索
|
||||
|
||||
コレクション内のすべてのビデオを横断して検索する:
|
||||
|
||||
```python
|
||||
coll = conn.get_collection()
|
||||
|
||||
# Search across all videos in the collection
|
||||
results = coll.search(
|
||||
query="product demo",
|
||||
search_type=SearchType.semantic,
|
||||
)
|
||||
|
||||
for shot in results.get_shots():
|
||||
print(f"Video: {shot.video_id} [{shot.start:.1f}s - {shot.end:.1f}s]")
|
||||
```
|
||||
|
||||
> **注意:** コレクションレベルの検索は `SearchType.semantic` のみをサポートする。`SearchType.keyword` または `SearchType.scene` を `coll.search()` と組み合わせると `NotImplementedError` が発生する。キーワードやシーン検索には代わりに個々のビデオで `video.search()` を使用する。
|
||||
|
||||
## 検索 + コンパイル
|
||||
|
||||
一致するクリップをインデックス化、検索し、単一の再生可能なストリームにコンパイルする:
|
||||
|
||||
```python
|
||||
video.index_spoken_words(force=True)
|
||||
results = video.search(query="your query", search_type=SearchType.semantic)
|
||||
stream_url = results.compile()
|
||||
print(stream_url)
|
||||
```
|
||||
|
||||
## ヒント
|
||||
|
||||
* **一度インデックス化、何度も検索**:インデックス作成は高コストな操作。一度インデックスが作成されれば、検索は速くなる。
|
||||
* **インデックスタイプを組み合わせる**:音声単語とシーンの両方をインデックス化して、同じビデオですべての検索タイプを有効にする。
|
||||
* **クエリの最適化**:セマンティック検索は単一のキーワードではなく説明的な自然言語フレーズで最もよく機能する。
|
||||
* **精度向上のためにキーワード検索を使用**:正確な用語照合が必要なときは、キーワード検索でセマンティックドリフトを避けられる。
|
||||
* **「結果なし」の処理**:一致するものがない場合、`video.search()` は `InvalidRequestError` を発生させる。常に検索呼び出しをtry/exceptで包み、`"No results found"` を空の結果セットとして扱うこと。
|
||||
* **シーン検索ノイズのフィルタリング**:あいまいなクエリの場合、セマンティックシーン検索は低関連性の結果を返す可能性がある。ノイズをフィルタリングするために `score_threshold=0.3`(またはより高い値)を使用する。
|
||||
* **べき等なインデックス作成**:`index_spoken_words(force=True)` を使用すると安全に再インデックス化できる。`index_scenes()` には `force` パラメータがない——try/exceptで包み、`re.search(r"id\s+([a-f0-9]+)", str(e))` を使用してエラーメッセージから既存の `scene_index_id` を抽出する。
|
||||
406
docs/ja-JP/skills/videodb/reference/streaming.md
Normal file
406
docs/ja-JP/skills/videodb/reference/streaming.md
Normal file
@@ -0,0 +1,406 @@
|
||||
# ストリーミングと再生
|
||||
|
||||
VideoDBはオンデマンドでストリーミングを生成し、任意の標準ビデオプレーヤーで即時再生できるHLS互換のURLを返す。レンダリング時間やエクスポート待ちは不要——編集、検索、合成されたコンテンツは即座にストリーミングできる。
|
||||
|
||||
## 前提条件
|
||||
|
||||
ビデオはストリーミングを生成するために**コレクションにアップロードされている必要がある**。検索ベースのストリーミングには、ビデオも**インデックス化されている必要がある**(音声単語および/またはシーン)。インデックス作成の詳細については [search.md](search.md) を参照。
|
||||
|
||||
## コアコンセプト
|
||||
|
||||
### ストリーミング生成
|
||||
|
||||
VideoDBのすべてのビデオ、検索結果、タイムラインは**ストリームURL**を生成できる。このURLはオンデマンドでコンパイルされるHLS(HTTPライブストリーミング)マニフェストを指す。
|
||||
|
||||
```python
|
||||
# From a video
|
||||
stream_url = video.generate_stream()
|
||||
|
||||
# From a timeline
|
||||
stream_url = timeline.generate_stream()
|
||||
|
||||
# From search results
|
||||
stream_url = results.compile()
|
||||
```
|
||||
|
||||
## 単一ビデオのストリーミング
|
||||
|
||||
### 基本再生
|
||||
|
||||
```python
|
||||
import videodb
|
||||
|
||||
conn = videodb.connect()
|
||||
coll = conn.get_collection()
|
||||
video = coll.get_video("your-video-id")
|
||||
|
||||
# Generate stream URL
|
||||
stream_url = video.generate_stream()
|
||||
print(f"Stream: {stream_url}")
|
||||
|
||||
# Open in default browser
|
||||
video.play()
|
||||
```
|
||||
|
||||
### 字幕付き
|
||||
|
||||
```python
|
||||
# Index and add subtitles first
|
||||
video.index_spoken_words(force=True)
|
||||
stream_url = video.add_subtitle()
|
||||
|
||||
# Returned URL already includes subtitles
|
||||
print(f"Subtitled stream: {stream_url}")
|
||||
```
|
||||
|
||||
### 特定のセグメント
|
||||
|
||||
タイムスタンプ範囲のタイムラインを渡すことでビデオの一部のみをストリーミングする:
|
||||
|
||||
```python
|
||||
# Stream seconds 10-30 and 60-90
|
||||
stream_url = video.generate_stream(timeline=[(10, 30), (60, 90)])
|
||||
print(f"Segment stream: {stream_url}")
|
||||
```
|
||||
|
||||
## タイムラインコンポジションのストリーミング
|
||||
|
||||
マルチアセットコンポジションを構築してリアルタイムでストリーミングする:
|
||||
|
||||
```python
|
||||
import videodb
|
||||
from videodb.timeline import Timeline
|
||||
from videodb.asset import VideoAsset, AudioAsset, ImageAsset, TextAsset, TextStyle
|
||||
|
||||
conn = videodb.connect()
|
||||
coll = conn.get_collection()
|
||||
|
||||
video = coll.get_video(video_id)
|
||||
music = coll.get_audio(music_id)
|
||||
|
||||
timeline = Timeline(conn)
|
||||
|
||||
# Main video content
|
||||
timeline.add_inline(VideoAsset(asset_id=video.id))
|
||||
|
||||
# Background music overlay (starts at second 0)
|
||||
timeline.add_overlay(0, AudioAsset(asset_id=music.id))
|
||||
|
||||
# Text overlay at the beginning
|
||||
timeline.add_overlay(0, TextAsset(
|
||||
text="Live Demo",
|
||||
duration=3,
|
||||
style=TextStyle(fontsize=48, fontcolor="white", boxcolor="#000000"),
|
||||
))
|
||||
|
||||
# Generate the composed stream
|
||||
stream_url = timeline.generate_stream()
|
||||
print(f"Composed stream: {stream_url}")
|
||||
```
|
||||
|
||||
**重要な注意事項:** `add_inline()` は `VideoAsset` のみを受け入れる。`AudioAsset`、`ImageAsset`、`TextAsset` には `add_overlay()` を使用する。
|
||||
|
||||
詳細なタイムライン編集については [editor.md](editor.md) を参照。
|
||||
|
||||
## 検索結果のストリーミング
|
||||
|
||||
すべての一致するクリップを含む単一のストリームに検索結果をコンパイルする:
|
||||
|
||||
```python
|
||||
from videodb import SearchType
|
||||
from videodb.exceptions import InvalidRequestError
|
||||
|
||||
video.index_spoken_words(force=True)
|
||||
try:
|
||||
results = video.search("key announcement", search_type=SearchType.semantic)
|
||||
|
||||
# Compile all matching shots into one stream
|
||||
stream_url = results.compile()
|
||||
print(f"Search results stream: {stream_url}")
|
||||
|
||||
# Or play directly
|
||||
results.play()
|
||||
except InvalidRequestError as exc:
|
||||
if "No results found" in str(exc):
|
||||
print("No matching announcement segments were found.")
|
||||
else:
|
||||
raise
|
||||
```
|
||||
|
||||
### 個別の検索結果をストリーミングする
|
||||
|
||||
```python
|
||||
from videodb.exceptions import InvalidRequestError
|
||||
|
||||
try:
|
||||
results = video.search("product demo", search_type=SearchType.semantic)
|
||||
for i, shot in enumerate(results.get_shots()):
|
||||
stream_url = shot.generate_stream()
|
||||
print(f"Hit {i+1} [{shot.start:.1f}s-{shot.end:.1f}s]: {stream_url}")
|
||||
except InvalidRequestError as exc:
|
||||
if "No results found" in str(exc):
|
||||
print("No product demo segments matched the query.")
|
||||
else:
|
||||
raise
|
||||
```
|
||||
|
||||
## オーディオ再生
|
||||
|
||||
オーディオコンテンツの署名付き再生URLを取得する:
|
||||
|
||||
```python
|
||||
audio = coll.get_audio(audio_id)
|
||||
playback_url = audio.generate_url()
|
||||
print(f"Audio URL: {playback_url}")
|
||||
```
|
||||
|
||||
## 完全なワークフロー例
|
||||
|
||||
### 検索からストリーミングへのパイプライン
|
||||
|
||||
単一のワークフローで検索、タイムラインコンポジション、ストリーミングを組み合わせる:
|
||||
|
||||
```python
|
||||
import videodb
|
||||
from videodb import SearchType
|
||||
from videodb.exceptions import InvalidRequestError
|
||||
from videodb.timeline import Timeline
|
||||
from videodb.asset import VideoAsset, TextAsset, TextStyle
|
||||
|
||||
conn = videodb.connect()
|
||||
coll = conn.get_collection()
|
||||
video = coll.get_video("your-video-id")
|
||||
|
||||
video.index_spoken_words(force=True)
|
||||
|
||||
# Search for key moments
|
||||
queries = ["introduction", "main demo", "Q&A"]
|
||||
timeline = Timeline(conn)
|
||||
timeline_offset = 0.0
|
||||
|
||||
for query in queries:
|
||||
try:
|
||||
results = video.search(query, search_type=SearchType.semantic)
|
||||
shots = results.get_shots()
|
||||
except InvalidRequestError as exc:
|
||||
if "No results found" in str(exc):
|
||||
shots = []
|
||||
else:
|
||||
raise
|
||||
|
||||
if not shots:
|
||||
continue
|
||||
|
||||
# Add the section label where this batch starts in the compiled timeline
|
||||
timeline.add_overlay(timeline_offset, TextAsset(
|
||||
text=query.title(),
|
||||
duration=2,
|
||||
style=TextStyle(fontsize=36, fontcolor="white", boxcolor="#222222"),
|
||||
))
|
||||
|
||||
for shot in shots:
|
||||
timeline.add_inline(
|
||||
VideoAsset(asset_id=shot.video_id, start=shot.start, end=shot.end)
|
||||
)
|
||||
timeline_offset += shot.end - shot.start
|
||||
|
||||
stream_url = timeline.generate_stream()
|
||||
print(f"Dynamic compilation: {stream_url}")
|
||||
```
|
||||
|
||||
### マルチビデオストリーム
|
||||
|
||||
異なるビデオからのクリップを単一のストリームに組み合わせる:
|
||||
|
||||
```python
|
||||
import videodb
|
||||
from videodb.timeline import Timeline
|
||||
from videodb.asset import VideoAsset
|
||||
|
||||
conn = videodb.connect()
|
||||
coll = conn.get_collection()
|
||||
|
||||
video_clips = [
|
||||
{"id": "vid_001", "start": 0, "end": 15},
|
||||
{"id": "vid_002", "start": 10, "end": 30},
|
||||
{"id": "vid_003", "start": 5, "end": 25},
|
||||
]
|
||||
|
||||
timeline = Timeline(conn)
|
||||
for clip in video_clips:
|
||||
timeline.add_inline(
|
||||
VideoAsset(asset_id=clip["id"], start=clip["start"], end=clip["end"])
|
||||
)
|
||||
|
||||
stream_url = timeline.generate_stream()
|
||||
print(f"Multi-video stream: {stream_url}")
|
||||
```
|
||||
|
||||
### 条件付きストリーミングアセンブリ
|
||||
|
||||
検索結果の可用性に基づいてストリームを動的に構築する:
|
||||
|
||||
```python
|
||||
import videodb
|
||||
from videodb import SearchType
|
||||
from videodb.exceptions import InvalidRequestError
|
||||
from videodb.timeline import Timeline
|
||||
from videodb.asset import VideoAsset, TextAsset, TextStyle
|
||||
|
||||
conn = videodb.connect()
|
||||
coll = conn.get_collection()
|
||||
video = coll.get_video("your-video-id")
|
||||
|
||||
video.index_spoken_words(force=True)
|
||||
|
||||
timeline = Timeline(conn)
|
||||
|
||||
# Try to find specific content; fall back to full video
|
||||
topics = ["opening remarks", "technical deep dive", "closing"]
|
||||
|
||||
found_any = False
|
||||
timeline_offset = 0.0
|
||||
for topic in topics:
|
||||
try:
|
||||
results = video.search(topic, search_type=SearchType.semantic)
|
||||
shots = results.get_shots()
|
||||
except InvalidRequestError as exc:
|
||||
if "No results found" in str(exc):
|
||||
shots = []
|
||||
else:
|
||||
raise
|
||||
|
||||
if shots:
|
||||
found_any = True
|
||||
timeline.add_overlay(timeline_offset, TextAsset(
|
||||
text=topic.title(),
|
||||
duration=2,
|
||||
style=TextStyle(fontsize=32, fontcolor="white", boxcolor="#1a1a2e"),
|
||||
))
|
||||
for shot in shots:
|
||||
timeline.add_inline(
|
||||
VideoAsset(asset_id=shot.video_id, start=shot.start, end=shot.end)
|
||||
)
|
||||
timeline_offset += shot.end - shot.start
|
||||
|
||||
if found_any:
|
||||
stream_url = timeline.generate_stream()
|
||||
print(f"Curated stream: {stream_url}")
|
||||
else:
|
||||
# Fall back to full video stream
|
||||
stream_url = video.generate_stream()
|
||||
print(f"Full video stream: {stream_url}")
|
||||
```
|
||||
|
||||
### ライブイベントのリキャップ
|
||||
|
||||
イベント録音を複数のセクションを持つストリーミング可能なリキャップに処理する:
|
||||
|
||||
```python
|
||||
import videodb
|
||||
from videodb import SearchType
|
||||
from videodb.exceptions import InvalidRequestError
|
||||
from videodb.timeline import Timeline
|
||||
from videodb.asset import VideoAsset, AudioAsset, ImageAsset, TextAsset, TextStyle
|
||||
|
||||
conn = videodb.connect()
|
||||
coll = conn.get_collection()
|
||||
|
||||
# Upload event recording
|
||||
event = coll.upload(url="https://example.com/event-recording.mp4")
|
||||
event.index_spoken_words(force=True)
|
||||
|
||||
# Generate background music
|
||||
music = coll.generate_music(
|
||||
prompt="upbeat corporate background music",
|
||||
duration=120,
|
||||
)
|
||||
|
||||
# Generate title image
|
||||
title_img = coll.generate_image(
|
||||
prompt="modern event recap title card, dark background, professional",
|
||||
aspect_ratio="16:9",
|
||||
)
|
||||
|
||||
# Build the recap timeline
|
||||
timeline = Timeline(conn)
|
||||
timeline_offset = 0.0
|
||||
|
||||
# Main video segments from search
|
||||
try:
|
||||
keynote = event.search("keynote announcement", search_type=SearchType.semantic)
|
||||
keynote_shots = keynote.get_shots()[:5]
|
||||
except InvalidRequestError as exc:
|
||||
if "No results found" in str(exc):
|
||||
keynote_shots = []
|
||||
else:
|
||||
raise
|
||||
if keynote_shots:
|
||||
keynote_start = timeline_offset
|
||||
for shot in keynote_shots:
|
||||
timeline.add_inline(
|
||||
VideoAsset(asset_id=shot.video_id, start=shot.start, end=shot.end)
|
||||
)
|
||||
timeline_offset += shot.end - shot.start
|
||||
else:
|
||||
keynote_start = None
|
||||
|
||||
try:
|
||||
demo = event.search("product demo", search_type=SearchType.semantic)
|
||||
demo_shots = demo.get_shots()[:5]
|
||||
except InvalidRequestError as exc:
|
||||
if "No results found" in str(exc):
|
||||
demo_shots = []
|
||||
else:
|
||||
raise
|
||||
if demo_shots:
|
||||
demo_start = timeline_offset
|
||||
for shot in demo_shots:
|
||||
timeline.add_inline(
|
||||
VideoAsset(asset_id=shot.video_id, start=shot.start, end=shot.end)
|
||||
)
|
||||
timeline_offset += shot.end - shot.start
|
||||
else:
|
||||
demo_start = None
|
||||
|
||||
# Overlay title card image
|
||||
timeline.add_overlay(0, ImageAsset(
|
||||
asset_id=title_img.id, width=100, height=100, x=80, y=20, duration=5
|
||||
))
|
||||
|
||||
# Overlay section labels at the correct timeline offsets
|
||||
if keynote_start is not None:
|
||||
timeline.add_overlay(max(5, keynote_start), TextAsset(
|
||||
text="Keynote Highlights",
|
||||
duration=3,
|
||||
style=TextStyle(fontsize=40, fontcolor="white", boxcolor="#0d1117"),
|
||||
))
|
||||
if demo_start is not None:
|
||||
timeline.add_overlay(max(5, demo_start), TextAsset(
|
||||
text="Demo Highlights",
|
||||
duration=3,
|
||||
style=TextStyle(fontsize=36, fontcolor="white", boxcolor="#0d1117"),
|
||||
))
|
||||
|
||||
# Overlay background music
|
||||
timeline.add_overlay(0, AudioAsset(
|
||||
asset_id=music.id, fade_in_duration=3
|
||||
))
|
||||
|
||||
# Stream the final recap
|
||||
stream_url = timeline.generate_stream()
|
||||
print(f"Event recap: {stream_url}")
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## ヒント
|
||||
|
||||
* **HLS互換性**:ストリームURLはHLSマニフェスト(`.m3u8`)を返す。Safariでネイティブに動作し、他のブラウザではhls.jsや類似のライブラリで動作する。
|
||||
* **オンデマンドコンパイル**:ストリーミングはリクエスト時にサーバーサイドでコンパイルされる。初回再生には短いコンパイル遅延が発生する場合がある;同じコンポジションの後続再生はキャッシュされる。
|
||||
* **キャッシング**:`video.generate_stream()`(引数なし)の2回目の呼び出しは再コンパイルせずにキャッシュされたストリームURLを返す。
|
||||
* **セグメントストリーム**:`video.generate_stream(timeline=[(start, end)])` は完全な `Timeline` オブジェクトを構築せずに特定のクリップをストリーミングする最速の方法。
|
||||
* **インラインとオーバーレイ**:`add_inline()` は `VideoAsset` のみを受け入れ、アセットをメイントラックに順番に配置する。`add_overlay()` は `AudioAsset`、`ImageAsset`、`TextAsset` を受け入れ、指定された開始時間にそれらを上にオーバーレイする。
|
||||
* **TextStyleのデフォルト**:`TextStyle` のデフォルトは `font='Sans'`、`fontcolor='black'`。テキストの背景色には `boxcolor`(`bgcolor` ではない)を使用する。
|
||||
* **生成との組み合わせ**:`coll.generate_music(prompt, duration)` と `coll.generate_image(prompt, aspect_ratio)` を使用してタイムラインコンポジションのアセットを作成する。
|
||||
* **再生**:`.play()` はデフォルトのシステムブラウザでストリームURLを開く。プログラム的な使用には直接URLの文字列を処理する。
|
||||
142
docs/ja-JP/skills/videodb/reference/use-cases.md
Normal file
142
docs/ja-JP/skills/videodb/reference/use-cases.md
Normal file
@@ -0,0 +1,142 @@
|
||||
# ユースケース
|
||||
|
||||
VideoDBが実現する一般的なワークフローと機能。コードの詳細については [api-reference.md](api-reference.md)、[capture.md](capture.md)、[editor.md](editor.md)、[search.md](search.md) を参照。
|
||||
|
||||
***
|
||||
|
||||
## ビデオ検索とハイライト
|
||||
|
||||
### ハイライトリールの作成
|
||||
|
||||
長いビデオ(会議講演、講義、カンファレンス録画)をアップロードし、トピック別(「製品発表」「Q&Aセッション」「デモ」)に主要なモーメントを検索し、一致するクリップを共有可能なハイライトリールに自動的にアセンブルする。
|
||||
|
||||
### 検索可能なビデオライブラリの構築
|
||||
|
||||
ビデオをコレクションに一括アップロードし、検索のために音声コンテンツをインデックス化し、ライブラリ全体でクエリを実行する。数百時間のコンテンツから特定のトピックを即座に見つける。
|
||||
|
||||
### 特定のクリップの抽出
|
||||
|
||||
クエリに一致するセグメントを検索し(「予算の議論」「アクションアイテム」)、各一致するセグメントを独自のストリームURLを持つ独立したクリップとして抽出する。
|
||||
|
||||
***
|
||||
|
||||
## ビデオの強化
|
||||
|
||||
### プロフェッショナルな仕上げを追加する
|
||||
|
||||
生の素材を取得して強化する:
|
||||
|
||||
* 音声に基づいて字幕を自動生成する
|
||||
* 特定のタイムスタンプにカスタムサムネイルを追加する
|
||||
* バックグラウンドミュージックオーバーレイ
|
||||
* 生成された画像を使用したオープニング/エンディングシーケンス
|
||||
|
||||
### AIで強化されたコンテンツ
|
||||
|
||||
既存のビデオと生成AIを組み合わせる:
|
||||
|
||||
* トランスクリプトコンテンツからテキストサマリーを生成する
|
||||
* ビデオの長さに合わせたバックグラウンドミュージックを作成する
|
||||
* タイトルカードとオーバーレイ画像を生成する
|
||||
* すべての要素を磨き上げた最終出力にミックスする
|
||||
|
||||
***
|
||||
|
||||
## リアルタイム録画(デスクトップ/会議)
|
||||
|
||||
### AIによる画面+オーディオ録画
|
||||
|
||||
画面、マイク、システムオーディオを同時にキャプチャする。リアルタイムで取得:
|
||||
|
||||
* **ライブ転写** - 音声を即座にテキストに変換
|
||||
* **オーディオサマリー** - 定期的に生成されるAIによる議論サマリー
|
||||
* **ビジュアルインデックス** - 画面アクティビティのAI説明
|
||||
|
||||
### サマリー機能付き会議録画
|
||||
|
||||
会議を録画して全参加者の発言をリアルタイムで転写する。主要な議論のポイント、決定事項、アクションアイテムを含む定期的なサマリーをリアルタイムで受け取る。
|
||||
|
||||
### 画面アクティビティ追跡
|
||||
|
||||
AIが生成した説明で画面アクティビティを追跡する:
|
||||
|
||||
* 「ユーザーはGoogle Sheetsでスプレッドシートを閲覧しています」
|
||||
* 「ユーザーはPythonファイルを含むコードエディターに切り替えました」
|
||||
* 「画面共有付きのビデオ通話が進行中です」
|
||||
|
||||
### セッション後の処理
|
||||
|
||||
録画が終わると、録音は永続的なビデオとしてエクスポートされる。その後:
|
||||
|
||||
* 検索可能なトランスクリプトを生成する
|
||||
* 録画内の特定のトピックを検索する
|
||||
* 重要なモーメントのクリップを抽出する
|
||||
* ストリームURLまたはプレーヤーリンクで共有する
|
||||
|
||||
***
|
||||
|
||||
## ライブストリームインテリジェンス(RTSP/RTMP)
|
||||
|
||||
### 外部ストリームへの接続
|
||||
|
||||
RTSPまたはRTMPソース(セキュリティカメラ、エンコーダー、ブロードキャスト)からライブビデオを取り込む。コンテンツをリアルタイムで処理してインデックス化する。
|
||||
|
||||
### リアルタイムイベント検出
|
||||
|
||||
ライブストリームで検出するイベントを定義する:
|
||||
|
||||
* 「制限区域に人が入った」
|
||||
* 「交差点での交通違反」
|
||||
* 「棚に製品が見える」
|
||||
|
||||
イベントが発生したときにWebSocketまたはWebhook経由でアラートを受け取る。
|
||||
|
||||
### ライブストリーム検索
|
||||
|
||||
録画されたライブストリームコンテンツ内を検索する。数時間の連続した素材から特定のモーメントを見つけてクリップを生成する。
|
||||
|
||||
***
|
||||
|
||||
## コンテンツモデレーションとセキュリティ
|
||||
|
||||
### 自動化されたコンテンツレビュー
|
||||
|
||||
AIを使用してビデオシーンをインデックス化し、問題のあるコンテンツを検索する。暴力、不適切なコンテンツ、またはポリシー違反を含むビデオにフラグを立てる。
|
||||
|
||||
### 不適切な言葉の検出
|
||||
|
||||
オーディオ内の不適切な言葉を検出して特定する。検出されたタイムスタンプにビープ音をオーバーレイするオプションもある。
|
||||
|
||||
***
|
||||
|
||||
## プラットフォーム統合
|
||||
|
||||
### ソーシャルメディア向けフォーマット変換
|
||||
|
||||
異なるプラットフォーム向けにビデオのフォーマットを変換する:
|
||||
|
||||
* 縦型(9:16):TikTok、Reels、Shorts向け
|
||||
* 正方形(1:1):Instagramフィード向け
|
||||
* 横型(16:9):YouTube向け
|
||||
|
||||
### 配信のためのトランスコード
|
||||
|
||||
異なる配信ターゲットに向けて解像度、ビットレート、品質を変更する。Web、モバイル、ブロードキャスト出力に最適化されたストリーム。
|
||||
|
||||
### 共有可能なリンクの生成
|
||||
|
||||
すべての操作は再生可能なストリームURLを生成する。Webプレーヤーへの埋め込み、直接共有、または既存のプラットフォームとの統合が可能。
|
||||
|
||||
***
|
||||
|
||||
## ワークフローのサマリー
|
||||
|
||||
| 目標 | VideoDBのアプローチ |
|
||||
|------|------------------|
|
||||
| ビデオ内のセグメントを見つける | 音声/シーンをインデックス化 → 検索 → クリップをアセンブル |
|
||||
| ハイライトリールを作成する | 複数のトピックを検索 → タイムラインを構築 → ストリームを生成 |
|
||||
| 字幕を追加する | 音声をインデックス化 → 字幕オーバーレイを追加 |
|
||||
| 画面録画 + AI | 録画を開始 → AIパイプラインを実行 → ビデオをエクスポート |
|
||||
| ライブストリームを監視する | RTSPに接続 → シーンをインデックス化 → アラートを作成 |
|
||||
| ソーシャルメディア向けにフォーマット変換 | ターゲットのアスペクト比にリフレーム |
|
||||
| クリップをマージする | 複数のアセットでタイムラインを構築 → ストリームを生成 |
|
||||
91
docs/ja-JP/skills/visa-doc-translate/README.md
Normal file
91
docs/ja-JP/skills/visa-doc-translate/README.md
Normal file
@@ -0,0 +1,91 @@
|
||||
# ビザ書類翻訳ツール
|
||||
|
||||
ビザ申請書類を画像からプロフェッショナルな英語PDFに自動翻訳する。
|
||||
|
||||
## 機能
|
||||
|
||||
* **自動OCR**:複数のOCR方法を試みる(macOS Vision、EasyOCR、Tesseract)
|
||||
* **バイリンガルPDF**:元の画像 + プロフェッショナルな英語翻訳
|
||||
* **多言語対応**:中国語およびその他の言語に対応
|
||||
* **プロフェッショナルなフォーマット**:公式ビザ申請に適合
|
||||
* **完全自動化**:人の介入不要
|
||||
|
||||
## 対応ファイルタイプ
|
||||
|
||||
* 銀行預金証明書(存款证明)
|
||||
* 在職証明書(在职证明)
|
||||
* 退職証明書(退休证明)
|
||||
* 収入証明書(收入证明)
|
||||
* 不動産証明書(房产证明)
|
||||
* 営業許可証(营业执照)
|
||||
* 身分証明書とパスポート
|
||||
|
||||
## 使用方法
|
||||
|
||||
```bash
|
||||
/visa-doc-translate <image-file>
|
||||
```
|
||||
|
||||
### 例
|
||||
|
||||
```bash
|
||||
/visa-doc-translate RetirementCertificate.PNG
|
||||
/visa-doc-translate BankStatement.HEIC
|
||||
/visa-doc-translate EmploymentLetter.jpg
|
||||
```
|
||||
|
||||
## 出力
|
||||
|
||||
`<filename>_Translated.pdf` を作成し、以下を含む:
|
||||
|
||||
* **第1ページ**:元の書類画像(中央配置、A4サイズ)
|
||||
* **第2ページ**:プロフェッショナルな英語翻訳
|
||||
|
||||
## 要件
|
||||
|
||||
### Pythonライブラリ
|
||||
|
||||
```bash
|
||||
pip install pillow reportlab
|
||||
```
|
||||
|
||||
### OCR(以下のいずれかが必要)
|
||||
|
||||
**macOS(推奨)**:
|
||||
|
||||
```bash
|
||||
pip install pyobjc-framework-Vision pyobjc-framework-Quartz
|
||||
```
|
||||
|
||||
**クロスプラットフォーム**:
|
||||
|
||||
```bash
|
||||
pip install easyocr
|
||||
```
|
||||
|
||||
**Tesseract**:
|
||||
|
||||
```bash
|
||||
brew install tesseract tesseract-lang
|
||||
pip install pytesseract
|
||||
```
|
||||
|
||||
## 仕組み
|
||||
|
||||
1. 必要に応じてHEICをPNGに変換する
|
||||
2. EXIFの回転を確認して適用する
|
||||
3. 利用可能なOCR方法でテキストを抽出する
|
||||
4. プロフェッショナルな英語に翻訳する
|
||||
5. バイリンガルPDFを生成する
|
||||
|
||||
## 最適な用途
|
||||
|
||||
* オーストラリアのビザ申請
|
||||
* 米国のビザ申請
|
||||
* カナダのビザ申請
|
||||
* 英国のビザ申請
|
||||
* EUのビザ申請
|
||||
|
||||
## ライセンス
|
||||
|
||||
MIT
|
||||
119
docs/ja-JP/skills/visa-doc-translate/SKILL.md
Normal file
119
docs/ja-JP/skills/visa-doc-translate/SKILL.md
Normal file
@@ -0,0 +1,119 @@
|
||||
---
|
||||
name: visa-doc-translate
|
||||
description: ビザ申請書類(画像)を英語に翻訳し、原文と翻訳を含むバイリンガルPDFを作成する
|
||||
---
|
||||
|
||||
ビザ申請のためのビザ申請書類の翻訳を支援している。
|
||||
|
||||
## 手順
|
||||
|
||||
ユーザーが画像ファイルのパスを提供した場合、**確認を求めずに**以下の手順を**自動的に**実行する:
|
||||
|
||||
1. **画像変換**:ファイルがHEIC形式の場合、`sips -s format png <input> --out <output>` を使用してPNGに変換する
|
||||
|
||||
2. **画像の回転**:
|
||||
* EXIFの向きデータを確認する
|
||||
* EXIFデータに基づいて画像を自動的に回転させる
|
||||
* EXIFの向きが6の場合は、反時計回りに90度回転させる
|
||||
* 必要に応じて追加の回転を適用する(ドキュメントが上下逆に見える場合は180度をテストする)
|
||||
|
||||
3. **OCRテキスト抽出**:
|
||||
* 複数のOCR方法を自動的に試みる:
|
||||
* macOS Visionフレームワーク(macOS優先)
|
||||
* EasyOCR(クロスプラットフォーム、tesseract不要)
|
||||
* Tesseract OCR(利用可能な場合)
|
||||
* ドキュメントからすべてのテキスト情報を抽出する
|
||||
* ドキュメントの種類を識別する(預金証明書、在職証明書、退職証明書など)
|
||||
|
||||
4. **翻訳**:
|
||||
* すべてのテキストコンテンツをプロフェッショナルに英語に翻訳する
|
||||
* 元のドキュメントの構造とフォーマットを維持する
|
||||
* ビザ申請に適した専門的な用語を使用する
|
||||
* 固有名詞は元の言語を保持し、括弧内に英語を追記する
|
||||
* 中国語の名前には拼音フォーマットを使用する(例:WU Zhengye)
|
||||
* すべての数字、日付、金額を正確に保持する
|
||||
|
||||
5. **PDF生成**:
|
||||
* PILとreportlabライブラリを使用してPythonスクリプトを作成する
|
||||
* 第1ページ:回転後の元の画像を中央に配置し、A4ページに合わせてスケーリングして表示する
|
||||
* 第2ページ:適切なフォーマットで英語翻訳を表示する:
|
||||
* タイトルは中央揃えで太字
|
||||
* コンテンツは左揃え、適切な間隔
|
||||
* 公式文書に適したプロフェッショナルなレイアウト
|
||||
* 下部に注記を追加する:"This is a certified English translation of the original document"
|
||||
* スクリプトを実行してPDFを生成する
|
||||
|
||||
6. **出力**:同じディレクトリに `<original_filename>_Translated.pdf` という名前のPDFファイルを作成する
|
||||
|
||||
## 対応ドキュメント
|
||||
|
||||
* 銀行預金証明書 (存款证明)
|
||||
* 収入証明書 (收入证明)
|
||||
* 在職証明書 (在职证明)
|
||||
* 退職証明書 (退休证明)
|
||||
* 不動産証明書 (房产证明)
|
||||
* 営業許可証 (营业执照)
|
||||
* 身分証明書とパスポート
|
||||
* その他の公式文書
|
||||
|
||||
## 技術実装
|
||||
|
||||
### OCR方法(順番に試す)
|
||||
|
||||
1. **macOS Visionフレームワーク**(macOSのみ):
|
||||
```python
|
||||
import Vision
|
||||
from Foundation import NSURL
|
||||
```
|
||||
|
||||
2. **EasyOCR**(クロスプラットフォーム):
|
||||
```bash
|
||||
pip install easyocr
|
||||
```
|
||||
|
||||
3. **Tesseract OCR**(利用可能な場合):
|
||||
```bash
|
||||
brew install tesseract tesseract-lang
|
||||
pip install pytesseract
|
||||
```
|
||||
|
||||
### 必要なPythonライブラリ
|
||||
|
||||
```bash
|
||||
pip install pillow reportlab
|
||||
```
|
||||
|
||||
macOS Visionフレームワークの場合:
|
||||
|
||||
```bash
|
||||
pip install pyobjc-framework-Vision pyobjc-framework-Quartz
|
||||
```
|
||||
|
||||
## 重要なガイドライン
|
||||
|
||||
* 各ステップでユーザーに確認を**求めない**
|
||||
* 最適な回転角度を自動的に判断する
|
||||
* 1つのOCR方法が失敗した場合は複数の方法を試みる
|
||||
* すべての数字、日付、金額が正確に翻訳されることを確認する
|
||||
* 簡潔でプロフェッショナルなフォーマットを使用する
|
||||
* プロセス全体を完了し、最終PDFの場所を報告する
|
||||
|
||||
## 使用例
|
||||
|
||||
```bash
|
||||
/visa-doc-translate RetirementCertificate.PNG
|
||||
/visa-doc-translate BankStatement.HEIC
|
||||
/visa-doc-translate EmploymentLetter.jpg
|
||||
```
|
||||
|
||||
## 出力例
|
||||
|
||||
このスキルは以下を行う:
|
||||
|
||||
1. 利用可能なOCR方法を使用してテキストを抽出する
|
||||
2. プロフェッショナルな英語に翻訳する
|
||||
3. 以下を含む `<filename>_Translated.pdf` を生成する:
|
||||
* 第1ページ:元のドキュメント画像
|
||||
* 第2ページ:プロフェッショナルな英語翻訳
|
||||
|
||||
オーストラリア、米国、カナダ、英国およびその他の国のビザ申請で翻訳書類が必要な場合に最適。
|
||||
125
docs/ja-JP/skills/workspace-surface-audit/SKILL.md
Normal file
125
docs/ja-JP/skills/workspace-surface-audit/SKILL.md
Normal file
@@ -0,0 +1,125 @@
|
||||
---
|
||||
name: workspace-surface-audit
|
||||
description: アクティブなリポジトリ、MCPサーバー、プラグイン、コネクター、環境サーフェス、ツールのセットアップを監査し、最も価値の高いECCネイティブスキル、フック、エージェント、オペレーターワークフローを推奨する。ユーザーがClaude Codeのセットアップを支援してほしい場合や、環境で実際に何が使えるかを理解したい場合に使用する。
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# ワークスペースサーフェス監査
|
||||
|
||||
読み取り専用の監査スキル。「このワークスペースとマシンが現在実際に何をできるか、次に何を追加または有効化すべきか?」という質問に答えるために使用する。
|
||||
|
||||
これはセットアップ監査プラグインに対するECCネイティブの回答である。ユーザーが明示的にフォローアップの実装を要求しない限り、ファイルを変更しない。
|
||||
|
||||
## 使用する場面
|
||||
|
||||
* ユーザーが「Claude Codeをセットアップする」「自動化を推奨する」「どのプラグインまたはMCPを使うべきか?」または「何が足りないか?」と言う
|
||||
* スキル、フック、コネクターをさらにインストールする前にマシンやリポジトリを監査する
|
||||
* 公式マーケットプラグインとECCネイティブのカバレッジを比較する
|
||||
* ワークフローレイヤーの欠落を見つけるために `.env`、`.mcp.json`、プラグイン設定、または接続されたアプリのサーフェスをレビューする
|
||||
* 機能がスキル、フック、エージェント、MCP、または外部コネクターのどれであるべきかを決定する
|
||||
|
||||
## 交渉不可能なルール
|
||||
|
||||
* シークレット値を決して印刷しない。プロバイダー名、機能名、ファイルパス、キーまたは設定が存在するかどうかのみを表示する。
|
||||
* ECCがそのサーフェスを合理的に所有できる場合は、一般的な「別のプラグインをインストール」という推奨よりECCネイティブワークフローを優先する。
|
||||
* 外部プラグインをベースラインとインスピレーションとして扱い、権威ある製品境界としてではない。
|
||||
* 3つのことを明確に区別する:
|
||||
* 現在利用可能なもの
|
||||
* 利用可能だがECCのカプセル化が不十分なもの
|
||||
* 利用不可能で新しい統合が必要なもの
|
||||
|
||||
## 監査の入力
|
||||
|
||||
質問に答えるために必要なファイルと設定のみを確認する:
|
||||
|
||||
1. リポジトリサーフェス
|
||||
* `package.json`、ロックファイル、言語マーカー、フレームワーク設定、`README.md`
|
||||
* `.mcp.json`、`.lsp.json`、`.claude/settings*.json`、`.codex/*`
|
||||
* `AGENTS.md`、`CLAUDE.md`、インストールマニフェスト、フック設定
|
||||
2. 環境サーフェス
|
||||
* アクティブなリポジトリと明らかに隣接するECCワークスペース内の `.env*` ファイル
|
||||
* キー名のみを表示する(`STRIPE_API_KEY`、`TWILIO_AUTH_TOKEN`、`FAL_KEY` など)
|
||||
3. 接続されたツールサーフェス
|
||||
* インストール済みプラグイン、有効化されたコネクター、MCPサーバー、LSP、アプリ統合
|
||||
4. ECCサーフェス
|
||||
* 要件をカバーする既存のスキル、コマンド、フック、エージェント、インストールモジュール
|
||||
|
||||
## 監査プロセス
|
||||
|
||||
### フェーズ1:既存のものを棚卸しする
|
||||
|
||||
簡潔なインベントリを生成する:
|
||||
|
||||
* アクティブなツールチェーンのターゲット
|
||||
* インストール済みプラグインと接続されたアプリ
|
||||
* 設定済みのMCPサーバー
|
||||
* 設定済みのLSPサーバー
|
||||
* キー名で示唆される環境ベースのサービス
|
||||
* ワークスペースに関連する既存のECCスキル
|
||||
|
||||
サーフェスが生の形式でしか存在しない場合は指摘する。例:
|
||||
|
||||
* 「Stripeは接続されたアプリで利用可能だが、ECCには課金操作スキルがない」
|
||||
* 「Google Driveは接続されているが、ECCにはGoogle Workspaceネイティブの操作ワークフローがない」
|
||||
|
||||
### フェーズ2:公式およびインストール済みサーフェスとベンチマーク比較する
|
||||
|
||||
ワークスペースを以下と比較する:
|
||||
|
||||
* セットアップ、レビュー、ドキュメント、デザイン、またはワークフロー品質と重複する公式Claudeプラグイン
|
||||
* ClaudeまたはCodexにローカルインストールされたプラグイン
|
||||
* ユーザーが現在接続しているアプリのサーフェス
|
||||
|
||||
名前だけを列挙しない。各比較に対して以下を答える:
|
||||
|
||||
1. それらが実際に何をするか
|
||||
2. ECCが同等の機能をすでに持っているか
|
||||
3. ECCが生の形式のみを持っているか
|
||||
4. ECCがそのワークフローを完全に欠いているか
|
||||
|
||||
### フェーズ3:ギャップをECCの決定に変換する
|
||||
|
||||
各実際のギャップについて、適切なECCネイティブの形式を推奨する:
|
||||
|
||||
| ギャップの種類 | 優先するECC形式 |
|
||||
|----------|---------------------|
|
||||
| 繰り返し可能な操作ワークフロー | スキル |
|
||||
| 自動実行または副作用 | フック |
|
||||
| 特殊な委任役割 | エージェント |
|
||||
| 外部ツールブリッジ | MCPサーバーまたはコネクター |
|
||||
| インストール/オンボーディングガイダンス | セットアップまたは監査スキル |
|
||||
|
||||
ニーズが運用的であってインフラストラクチャ的でない場合は、デフォルトで既存のツールをオーケストレーションするユーザー向けスキルを使用する。
|
||||
|
||||
## 出力フォーマット
|
||||
|
||||
この順序で5つのセクションを返す:
|
||||
|
||||
1. **現在のサーフェス**
|
||||
* 現在利用可能なもの
|
||||
2. **同等の機能**
|
||||
* ECCがベースラインに一致または超えている場所
|
||||
3. **生の形式のみのギャップ**
|
||||
* ツールは存在するが、ECCには簡潔な操作スキルがない
|
||||
4. **欠けている統合**
|
||||
* まだ利用できない機能
|
||||
5. **上位3-5の次のステップ**
|
||||
* 影響度順の具体的なECCネイティブな追加
|
||||
|
||||
## 推奨ルール
|
||||
|
||||
* 各カテゴリにつき最大1-2つの最も価値の高いアイデアを推奨する。
|
||||
* 明確なユーザー意図とビジネス価値を持つスキルを優先する:
|
||||
* セットアップ監査
|
||||
* 課金/顧客運用
|
||||
* Issue/プロジェクト運用
|
||||
* Google Workspace運用
|
||||
* デプロイ/運用コントロール
|
||||
* コネクターが企業固有の場合、それが実際に利用可能またはユーザーのワークフローに明らかに有用な場合のみ推奨する。
|
||||
* ECCがすでに強力な生の形式を持っている場合は、まったく新しいサブシステムを発明するのではなくカプセル化スキルを提案する。
|
||||
|
||||
## 良い結果
|
||||
|
||||
* ユーザーが接続されているもの、欠けているもの、ECCが次に持つべきものをすぐに確認できる。
|
||||
* 推奨事項は再発見なしにリポジトリで実装できるほど具体的。
|
||||
* 最終的な回答はAPIブランドではなくワークフローを中心に整理されている。
|
||||
210
docs/ja-JP/skills/x-api/SKILL.md
Normal file
210
docs/ja-JP/skills/x-api/SKILL.md
Normal file
@@ -0,0 +1,210 @@
|
||||
---
|
||||
name: x-api
|
||||
description: ツイートの投稿、スレッド、タイムラインの読み取り、検索、分析のためのX/Twitter API統合。OAuth認証パターン、レートリミット、プラットフォームネイティブなコンテンツ投稿をカバーする。ユーザーがプログラムでXと対話したい場合に使用する。
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# X API
|
||||
|
||||
投稿、読み取り、検索、分析のためにX(Twitter)とプログラムで対話する。
|
||||
|
||||
## 有効化する場面
|
||||
|
||||
* ユーザーがプログラムでツイートやスレッドを投稿したい
|
||||
* Xのタイムライン、メンション、またはユーザーデータを読み取る
|
||||
* X上でコンテンツ、トレンド、または会話を検索する
|
||||
* X統合またはボットを構築する
|
||||
* 分析とエンゲージメント追跡
|
||||
* ユーザーが「Xに投稿する」「ツイートする」「X API」または「Twitter API」と言及している
|
||||
|
||||
## 認証
|
||||
|
||||
### OAuth 2.0 Bearerトークン(アプリのみ)
|
||||
|
||||
最適な用途:読み取り集中の操作、検索、公開データ。
|
||||
|
||||
```bash
|
||||
# Environment setup
|
||||
export X_BEARER_TOKEN="your-bearer-token"
|
||||
```
|
||||
|
||||
```python
|
||||
import os
|
||||
import requests
|
||||
|
||||
bearer = os.environ["X_BEARER_TOKEN"]
|
||||
headers = {"Authorization": f"Bearer {bearer}"}
|
||||
|
||||
# Search recent tweets
|
||||
resp = requests.get(
|
||||
"https://api.x.com/2/tweets/search/recent",
|
||||
headers=headers,
|
||||
params={"query": "claude code", "max_results": 10}
|
||||
)
|
||||
tweets = resp.json()
|
||||
```
|
||||
|
||||
### OAuth 1.0a(ユーザーコンテキスト)
|
||||
|
||||
以下に必要:ツイートの投稿、アカウント管理、DM。
|
||||
|
||||
```bash
|
||||
# Environment setup — source before use
|
||||
export X_API_KEY="your-api-key"
|
||||
export X_API_SECRET="your-api-secret"
|
||||
export X_ACCESS_TOKEN="your-access-token"
|
||||
export X_ACCESS_SECRET="your-access-secret"
|
||||
```
|
||||
|
||||
```python
|
||||
import os
|
||||
from requests_oauthlib import OAuth1Session
|
||||
|
||||
oauth = OAuth1Session(
|
||||
os.environ["X_API_KEY"],
|
||||
client_secret=os.environ["X_API_SECRET"],
|
||||
resource_owner_key=os.environ["X_ACCESS_TOKEN"],
|
||||
resource_owner_secret=os.environ["X_ACCESS_SECRET"],
|
||||
)
|
||||
```
|
||||
|
||||
## コア操作
|
||||
|
||||
### ツイートを1件投稿する
|
||||
|
||||
```python
|
||||
resp = oauth.post(
|
||||
"https://api.x.com/2/tweets",
|
||||
json={"text": "Hello from Claude Code"}
|
||||
)
|
||||
resp.raise_for_status()
|
||||
tweet_id = resp.json()["data"]["id"]
|
||||
```
|
||||
|
||||
### スレッドを投稿する
|
||||
|
||||
```python
|
||||
def post_thread(oauth, tweets: list[str]) -> list[str]:
|
||||
ids = []
|
||||
reply_to = None
|
||||
for text in tweets:
|
||||
payload = {"text": text}
|
||||
if reply_to:
|
||||
payload["reply"] = {"in_reply_to_tweet_id": reply_to}
|
||||
resp = oauth.post("https://api.x.com/2/tweets", json=payload)
|
||||
tweet_id = resp.json()["data"]["id"]
|
||||
ids.append(tweet_id)
|
||||
reply_to = tweet_id
|
||||
return ids
|
||||
```
|
||||
|
||||
### ユーザーのタイムラインを読み取る
|
||||
|
||||
```python
|
||||
resp = requests.get(
|
||||
f"https://api.x.com/2/users/{user_id}/tweets",
|
||||
headers=headers,
|
||||
params={
|
||||
"max_results": 10,
|
||||
"tweet.fields": "created_at,public_metrics",
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
### ツイートを検索する
|
||||
|
||||
```python
|
||||
resp = requests.get(
|
||||
"https://api.x.com/2/tweets/search/recent",
|
||||
headers=headers,
|
||||
params={
|
||||
"query": "from:affaanmustafa -is:retweet",
|
||||
"max_results": 10,
|
||||
"tweet.fields": "public_metrics,created_at",
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
### ユーザー名でユーザーを取得する
|
||||
|
||||
```python
|
||||
resp = requests.get(
|
||||
"https://api.x.com/2/users/by/username/affaanmustafa",
|
||||
headers=headers,
|
||||
params={"user.fields": "public_metrics,description,created_at"}
|
||||
)
|
||||
```
|
||||
|
||||
### メディアをアップロードして投稿する
|
||||
|
||||
```python
|
||||
# Media upload uses v1.1 endpoint
|
||||
|
||||
# Step 1: Upload media
|
||||
media_resp = oauth.post(
|
||||
"https://upload.twitter.com/1.1/media/upload.json",
|
||||
files={"media": open("image.png", "rb")}
|
||||
)
|
||||
media_id = media_resp.json()["media_id_string"]
|
||||
|
||||
# Step 2: Post with media
|
||||
resp = oauth.post(
|
||||
"https://api.x.com/2/tweets",
|
||||
json={"text": "Check this out", "media": {"media_ids": [media_id]}}
|
||||
)
|
||||
```
|
||||
|
||||
## レートリミット
|
||||
|
||||
X APIのレートリミットはエンドポイント、認証方法、アカウントティアによって異なり、時間とともに変化する。常に:
|
||||
|
||||
* ハードコードされた仮定を立てる前に現在のX開発者ドキュメントを確認する
|
||||
* 実行時に `x-rate-limit-remaining` と `x-rate-limit-reset` ヘッダーを読み取る
|
||||
* コード内の静的テーブルに頼らず、自動的にバックオフする
|
||||
|
||||
```python
|
||||
import time
|
||||
|
||||
remaining = int(resp.headers.get("x-rate-limit-remaining", 0))
|
||||
if remaining < 5:
|
||||
reset = int(resp.headers.get("x-rate-limit-reset", 0))
|
||||
wait = max(0, reset - int(time.time()))
|
||||
print(f"Rate limit approaching. Resets in {wait}s")
|
||||
```
|
||||
|
||||
## エラーハンドリング
|
||||
|
||||
```python
|
||||
resp = oauth.post("https://api.x.com/2/tweets", json={"text": content})
|
||||
if resp.status_code == 201:
|
||||
return resp.json()["data"]["id"]
|
||||
elif resp.status_code == 429:
|
||||
reset = int(resp.headers["x-rate-limit-reset"])
|
||||
raise Exception(f"Rate limited. Resets at {reset}")
|
||||
elif resp.status_code == 403:
|
||||
raise Exception(f"Forbidden: {resp.json().get('detail', 'check permissions')}")
|
||||
else:
|
||||
raise Exception(f"X API error {resp.status_code}: {resp.text}")
|
||||
```
|
||||
|
||||
## セキュリティ
|
||||
|
||||
* **トークンをハードコードしない。** 環境変数または `.env` ファイルを使用する。
|
||||
* **`.env` ファイルをコミットしない。** `.gitignore` に追加する。
|
||||
* **トークンが漏洩した場合はローテーションする。** developer.x.comで再生成する。
|
||||
* **書き込み権限が不要な場合は読み取り専用トークンを使用する。**
|
||||
* **OAuthシークレットを安全に保管する** — ソースコードやログに保存しない。
|
||||
|
||||
## コンテンツエンジンとの統合
|
||||
|
||||
`content-engine` スキルを使用してプラットフォームネイティブなコンテンツを生成し、X API経由で投稿する:
|
||||
|
||||
1. コンテンツエンジンを使用してコンテンツを生成する(Xプラットフォームフォーマット)
|
||||
2. 長さを検証する(ツイート1件あたり280文字)
|
||||
3. 上記のパターンを使用してX API経由で投稿する
|
||||
4. public\_metricsでエンゲージメントを追跡する
|
||||
|
||||
## 関連スキル
|
||||
|
||||
* `content-engine` — X向けのプラットフォームネイティブコンテンツを生成する
|
||||
* `crosspost` — X、LinkedIn、その他のプラットフォームでコンテンツを配信する
|
||||
Reference in New Issue
Block a user