diff options
Diffstat (limited to 'app/lib/claude.server.ts')
| -rw-r--r-- | app/lib/claude.server.ts | 125 |
1 files changed, 125 insertions, 0 deletions
diff --git a/app/lib/claude.server.ts b/app/lib/claude.server.ts new file mode 100644 index 0000000..4dcce6e --- /dev/null +++ b/app/lib/claude.server.ts @@ -0,0 +1,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); +} |
