香り情報の抽出方法
本ドキュメントでは、源氏物語のXMLテキストから香りに関する情報を抽出する方法について説明します。
概要
scripts/extract-smells-with-openrouter.jsスクリプトを使用して、源氏物語のTEI-XMLファイルから香りに関する記述を自動抽出し、構造化されたJSONデータを生成します。
必要な環境
前提条件
- Node.js (v18以上推奨)
- OpenRouter APIアクセス
環境変数の設定
.envファイルに以下の環境変数を設定してください:
OPENROUTER_API_KEY=your-api-key-here
使用方法
基本的な実行
# 全巻を処理
node scripts/extract-smells-with-openrouter.js
# 特定の巻のみ処理
node scripts/extract-smells-with-openrouter.js --volume=23
# デバッグモード(プロンプトを保存)
node scripts/extract-smells-with-openrouter.js --volume=23 --debug
実行例
# 全XMLファイルを処理
node scripts/extract-smells-with-openrouter.js
# 第23巻のみ処理
node scripts/extract-smells-with-openrouter.js --volume=23
# デバッグモードで実行(プロンプトをprompts/ディレクトリに保存)
node scripts/extract-smells-with-openrouter.js --volume=23 --debug
抽出される情報
スクリプトは以下の情報を各香り記述から抽出します:
| フィールド | 説明 | 例 |
|---|---|---|
id | 一意識別子 | "23-763-01" |
Book | 書籍名 | "校異源氏物語" |
Smell_Word | 香りを表す言葉 | "梅のか, にほひ" |
Smell_Source | 香りの源 | "梅の花, 御簾の中の人物の衣や室内の焚き香" |
Quality | 香りの性質・質 | "いける仏のみくに (生きている仏の国のよう)" |
Odour_Carrier | 香りの媒体 | "風" |
Evoked_Odorant | 連想される匂い物質 | "梅, 練香" |
Location | 場所 | "春の御殿(紫の上の御殿)" |
Perceiver | 知覚者 | "語り手" |
Time | 時間 | "春の朝" |
Circumstances | 状況 | "春の御殿の様子。とりわけ梅の香が..." |
Effect | 効果・影響 | "いける仏のみくにとおほゆ" |
SentenceBefore | 前の文 | 文脈の前の部分 |
Sentence | 該当文 | 香りが記述された文 |
SentenceAfter | 後の文 | 文脈の後の部分 |
Sentence_ModernJapanese | 現代語訳 | AIによる現代語訳 |
Sentence_English | 英訳 | AIによる英語訳 |
pb | ページ番号 | "763" |
vol | 巻番号 | "23" |
処理の流れ
1. XMLの前処理
- TEI-XML形式のファイルを読み込み
<body>要素のみを抽出corresp属性を削除してXMLサイズを削減- 簡略化されたXMLを生成
2. AIによる香り情報の抽出
Google Gemini 2.5 Pro(OpenRouter経由)を使用して以下のタスクを実行:
- 香り表現の検出: テキスト中の嗅覚に関する記述のみを特定
- 誤抽出の防止: 視覚的美しさ(「にほひ」の視覚的用法など)を除外
- メタデータの抽出: 構造化情報を抽出
- 翻訳の生成: 現代語訳と英訳を生成
3. プロンプトテンプレート
スクリプトは詳細なプロンプトテンプレートを使用:
- 嗅覚的な香りのみを抽出するよう厳格に指示
- 視覚的表現(「にほひやか」など)の除外ルールを明示
- 禁止事項リストで誤抽出を防止
- JSON配列形式での出力を要求
あなたは平安文学、特に『源氏物語』の専門家です。これからTEI形式でエンコードされた『源氏物語』のXMLテキスト文字列を提供します。
あなたのタスクは、このテキストから「嗅覚に関する香りの描写」のみを抽出し、指定されたJSONフォーマットの配列として出力することです。
# 指示事項
1. **抽出対象**: 嗅覚に関する「香り」の描写のみを対象とします。視覚的な美しさや気品、雰囲気などの描写は**絶対に含めないでください**。
2. **出力**: 見つかった描写を、指定のJSONフォーマットに従って配列 `[]` の中に `{}` オブジェクトとして格納してください。該当する描写が一つも見つからない場合は、空の配列 `[]` を返してください。
3. **周辺文の取得**: `SentenceBefore` と `SentenceAfter` は、該当する `<seg>` 要素の直前と直後の `<seg>` 要素の内容をそのまま入れてください。
4. **現代語訳と英訳**: `Sentence_ModernJapanese` と `Sentence_English` は、原文の内容を忠実に、かつ自然に翻訳してください。**原文にない「香り」という言葉を補ってはいけません**。
# 最重要の禁止事項 (誤抽出を避けるための厳格なルール)
以下の単語や表現は、多くの場合「嗅覚的な香り」を指しません。これらを抽出候補とする場合は、文脈が明確に嗅覚を示している場合に限定してください。少しでも曖昧な場合は抽出しないでください。
* **`にほひ` / `にほふ` / `にほひやか`**:
* **禁止**: 人物の容姿、衣装の色、光、絵画などを「美しい」「華やかだ」「輝いている」と表現している場合は**抽出禁止**です。
* **許可**: 「香りが満ちる(`みつ`)」「香りが移る(`うつる`)」「香りが燻る(`くゆる`)」「香ばしい(`かうばし`)」といった、明確に嗅覚的状況を示す言葉と共起する場合のみ抽出を検討します。
* **`かほ`**: 「顔」や「様子・感じ」を意味する場合は**抽出禁止**です。
* **`けはひ`**: 「気配」「様子」「雰囲気」を意味する場合は**抽出禁止**です。
* **`きよら` / `さやか`**: 視覚的な美しさや清潔さを表す場合は**抽出禁止**です。
* **`たえ` / `きぬのすそたえ`**: 衣の裾が破れる意味で、香りではありません。
* **`つほせんさい(壺千歳)`**: 植物・花の名前であり、香炉や香木ではありません。「御覧する」と組み合わさる場合は視覚的描写です。
* **`露` / `露けき`**: 湿気・水滴であり、香りではありません。
* **`すゝしき`**: 涼しさ(触覚)であり、香りではありません。
* **`けしき`**: 様子・雰囲気であり、香りではありません。
* **比喩表現**: 「つゆけき」(露けき、涙で湿っている様子)や、「けふり」(煙、火葬の煙)のような、直接的な香りの描写ではない比喩的・象徴的な表現は**抽出禁止**です。
# JSONフォーマット
\```json
[
{
"id": "帖番号-通し番号",
"Book": "校異源氏物語",
"Smell_Word": "香りを示す中心的な単語・フレーズ",
"Smell_Source": "香りの発生源",
"Quality": "香りの質(例:いみじ、かうばし、なつかし)",
"Odour_Carrier": "香りを運ぶ媒体(例:風)",
"Evoked_Odorant": "連想される香りの物質",
"Location": "香りが知覚された場所",
"Perceiver": "香りをかいでいる人物",
"Time": "時間帯",
"Circumstances": "どのような状況か(原文を引用・要約)",
"Effect": "香りによる効果や人物の反応",
"SentenceBefore": "該当箇所の直前の<seg>の内容",
"Sentence": "該当の<seg>の内容",
"SentenceAfter": "該当箇所の直後の<seg>の内容",
"Sentence_ModernJapanese": "Sentenceの現代語訳",
"Sentence_English": "Sentenceの英訳",
"pb": "<pb>要素のn属性値",
"vol": "帖番号(01, 02...)"
}
]
\```
**JSON配列のみを出力してください。説明文は不要です。**
</details>
4. 結果の保存
- 各巻ごとにJSON配列として保存(
src/data/smells-openrouter/[巻番号].json) - 全巻の結果を統合(
src/data/smells-openrouter-all.json) - インクリメンタル処理対応(既に処理済みの巻はスキップ)
出力形式
[
{
"id": "23-763-01",
"Book": "校異源氏物語",
"Smell_Word": "梅のか, にほひ",
"Smell_Source": "梅の花, 御簾の中の人物の衣や室内の焚き香",
"Quality": "いける仏のみくに (生きている仏の国のよう)",
"Odour_Carrier": "風",
"Evoked_Odorant": "梅, 練香",
"Location": "春の御殿(紫の上の御殿)",
"Perceiver": "語り手",
"Time": "春の朝",
"Circumstances": "春の御殿の様子。とりわけ梅の香が御簾の中の香りと吹き混じって...",
"Effect": "いける仏のみくにとおほゆ (生きている仏の国のようだと感じられる)",
"SentenceBefore": "ましたまへる御方〱の有さまゝねひたてんもことのはたるましくなん春のお",
"Sentence": "とゝの御まへとりわきて梅のかもみすのうちのにほひにふきまかひていける仏",
"SentenceAfter": "のみくにとおほゆさすかにうちとけてやすらかにすみなし給へりさふらふ人",
"Sentence_ModernJapanese": "春の御殿はとりわけ、梅の香が御簾の中の香りと吹き混じって、まるで生きている仏の国のようである。",
"Sentence_English": "In the Spring Palace, especially, the fragrance of plum blossoms blows and mingles with the scent from within the blinds, like the pure land of a living Buddha.",
"pb": "763",
"vol": "23"
}
]
パフォーマンスとコスト
API利用
- サービス: OpenRouter
- モデル: Google Gemini 2.5 Pro
- レート制限: 各巻処理後に2秒待機
- 温度設定:
temperature: 0(一貫性重視) - 最大トークン: 16,384(コンテキスト制限を考慮)
推定コスト
1巻の処理で:
- 1 APIリクエスト(巻全体を一度に処理)
- 処理時間: 約30秒〜1分
- コスト: OpenRouterのGemini 2.5 Pro料金に準拠
XML前処理による最適化
<body>要素のみを抽出corresp属性を削除- XMLサイズを大幅に削減(トークン数削減)
トラブルシューティング
よくあるエラー
-
XMLファイルが見つからない
巻番号 XX のファイルが見つかりません→ XMLファイルのパスを確認(デフォルト:
./xml/*.xml) -
API認証エラー
API request failed with status 401→
.envファイルのOPENROUTER_API_KEYを確認 -
JSONパースエラー
JSON parse failed→ AIの出力形式を確認、マークダウンコードブロックから抽出を試行
-
body要素が見つからない
body要素が見つかりません→ XMLファイルの構造を確認
デバッグ方法
--debugフラグでプロンプトをprompts/ディレクトリに保存- コンソールに処理中の巻番号とXMLサイズが表示されます
- 各巻の結果は
src/data/smells-openrouter/[巻番号].jsonに保存されます
データの後処理
抽出後のデータは以下のコマンドでインデックス化:
# データをsmells-index.jsonに統合
node scripts/build-smells-index.js
このスクリプトは:
src/data/smells-openrouter/内の全JSONファイルを読み込み- IDを生成(
[巻番号]-[ページ番号]-[通し番号]形式) Bookフィールドを「校異源氏物語」に統一src/data/smells-index.jsonに統合
参考
- OpenRouter公式サイト
- OpenRouter API Documentation
- TEI-XML仕様
- 技術ドキュメント: OpenRouter APIの使い方
- プロジェクトルートの
README.md