1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
|
import { spawn } from "child_process";
import Anthropic from "@anthropic-ai/sdk";
import type { Band } from "./db.server";
export interface BandMatch {
extracted: string;
matched_id: string | null;
matched_name: string | null;
confidence: "exact" | "fuzzy" | "new";
}
// ── Shared logic ──────────────────────────────────────────────────────────────
function buildPrompt(liveText: string, existingBands: Band[]): string {
const bandList = existingBands.map((b) => ` ${b.id}\t${b.name}`).join("\n");
return `ライブ情報テキストからバンド・アーティスト名を抽出し、既存バンドリストと照合してください。
## ライブ情報テキスト
${liveText}
## 既存バンドリスト(UUID\t名前)
${bandList || " (なし)"}
## タスク
1. ライブ情報からバンド・アーティスト名をすべて抽出する
2. 各名前を既存バンドリストと照合する(表記揺れ・全角半角・英語/日本語表記の違いなどを考慮)
3. 以下のJSON配列のみを返す(説明文不要):
[
{
"extracted": "抽出した名前(元テキストのまま)",
"matched_id": "一致した既存バンドのUUID(なければnull)",
"matched_name": "一致した既存バンドの名前(なければnull)",
"confidence": "exact | fuzzy | new"
}
]
confidenceの定義:
- "exact": 完全一致または明らかに同一
- "fuzzy": 表記揺れの可能性が高い(人間の確認が必要)
- "new": 既存リストに存在しない新しいバンド`;
}
function parseOutput(text: string): BandMatch[] {
const jsonMatch = text.match(/\[[\s\S]*\]/);
if (!jsonMatch) throw new Error("予期しない出力形式です");
return JSON.parse(jsonMatch[0]) as BandMatch[];
}
// ── Adapter interface ─────────────────────────────────────────────────────────
interface BandMatchAdapter {
parseLiveInfoBands(liveText: string, existingBands: Band[]): Promise<BandMatch[]>;
}
// ── Anthropic API adapter ─────────────────────────────────────────────────────
class AnthropicAPIAdapter implements BandMatchAdapter {
async parseLiveInfoBands(liveText: string, existingBands: Band[]): Promise<BandMatch[]> {
const client = new Anthropic();
const response = await client.messages.create({
model: "claude-sonnet-4-6",
max_tokens: 2048,
messages: [{ role: "user", content: buildPrompt(liveText, existingBands) }],
});
const text = response.content[0].type === "text" ? response.content[0].text : "";
return parseOutput(text);
}
}
// ── Claude Code CLI adapter ───────────────────────────────────────────────────
class ClaudeCodeCLIAdapter implements BandMatchAdapter {
async parseLiveInfoBands(liveText: string, existingBands: Band[]): Promise<BandMatch[]> {
const text = await this.runCLI(buildPrompt(liveText, existingBands));
return parseOutput(text);
}
private runCLI(prompt: string): Promise<string> {
return new Promise((resolve, reject) => {
const proc = spawn(
"claude",
["--print", "--output-format", "json", "--no-session-persistence", "--model", "sonnet"],
{ stdio: ["pipe", "pipe", "pipe"] }
);
let stdout = "";
let stderr = "";
proc.stdout.on("data", (d: Buffer) => { stdout += d.toString(); });
proc.stderr.on("data", (d: Buffer) => { stderr += d.toString(); });
proc.on("error", (err) => reject(new Error(`claude CLI 起動失敗: ${err.message}`)));
proc.on("close", (code) => {
if (code !== 0) {
reject(new Error(`claude CLI がコード ${code} で終了: ${stderr.trim()}`));
return;
}
try {
// --output-format json wraps result in { "result": "..." }
const json = JSON.parse(stdout) as { result?: string };
resolve(json.result ?? stdout);
} catch {
resolve(stdout);
}
});
proc.stdin.write(prompt);
proc.stdin.end();
});
}
}
// ── Factory ───────────────────────────────────────────────────────────────────
function getAdapter(): BandMatchAdapter {
const type = (process.env.CLAUDE_ADAPTER ?? "api").toLowerCase();
if (type === "cli") return new ClaudeCodeCLIAdapter();
return new AnthropicAPIAdapter();
}
// ── Public API (unchanged for callers) ───────────────────────────────────────
export function parseLiveInfoBands(liveText: string, existingBands: Band[]): Promise<BandMatch[]> {
return getAdapter().parseLiveInfoBands(liveText, existingBands);
}
|