gts-x / src /lib /editor /nli /engine.ts
JAMES HAN
feat: add GCP Vertex AI auth for Claude (alongside direct API key)
5e35f2a
Raw
History Blame Contribute Delete
3.39 kB
/**
* NLI Engine — Sends user instructions + subtitle context to Claude,
* receives structured tool calls back.
*/
import { getClaudeClient } from "../../ai/claude-client";
import { NLI_TOOLS } from "./tools";
import type { SubtitleData } from "../event-bus";
export interface NliRequest {
instruction: string;
subtitles: SubtitleData[];
fps: number;
sourceLanguage?: string;
targetLanguage?: string;
clientId?: string;
maxCPL?: number;
}
export interface NliToolCall {
toolName: string;
input: Record<string, unknown>;
}
export interface NliResult {
toolCalls: NliToolCall[];
reasoning: string;
}
const SYSTEM_PROMPT = `You are the NLI (Natural Language Interface) engine for GTS-X, a professional subtitle editor. You receive editing instructions in natural language and translate them into precise tool calls that modify subtitles.
You have access to the complete subtitle file as context. Analyze ALL subtitles when the instruction is contextual (e.g., character relationships, tone adjustments, consistency fixes).
IMPORTANT RULES:
- For deterministic operations (timecode shifts, find/replace, FPS conversion), use the appropriate deterministic tool. Do NOT use edit_subtitles for these.
- For semantic/contextual operations (tone adjustment, translation improvement, character voice), use edit_subtitles with the COMPLETE new text for each modified subtitle.
- When editing text, preserve subtitle formatting (line breaks for multi-line subtitles, tags).
- When fixing CPL violations, keep the meaning while shortening. Split into two lines if needed (use \\n for line breaks).
- Always provide a reason for each edit when using edit_subtitles.
- You may call multiple tools in one response if the instruction requires it.`;
function formatSubtitlesForContext(subtitles: SubtitleData[]): string {
return subtitles
.map(
(s, i) =>
`[${i}] ${s.speaker ? `(${s.speaker}) ` : ""}${s.tcIn}${s.tcOut} | ${s.text.replace(/\n/g, " / ")}`,
)
.join("\n");
}
export async function planNliExecution(
request: NliRequest,
): Promise<NliResult> {
const { client } = getClaudeClient();
const contextParts: string[] = [
`FPS: ${request.fps}`,
];
if (request.sourceLanguage)
contextParts.push(`Source: ${request.sourceLanguage}`);
if (request.targetLanguage)
contextParts.push(`Target: ${request.targetLanguage}`);
if (request.clientId) contextParts.push(`Client: ${request.clientId}`);
if (request.maxCPL) contextParts.push(`Max CPL: ${request.maxCPL}`);
const userMessage = `${contextParts.join(" | ")}
Total subtitles: ${request.subtitles.length}
SUBTITLES:
${formatSubtitlesForContext(request.subtitles)}
INSTRUCTION: ${request.instruction}`;
const response = await client.messages.create({
model: "claude-sonnet-4-20250514",
max_tokens: 8192,
system: SYSTEM_PROMPT,
tools: NLI_TOOLS,
messages: [{ role: "user", content: userMessage }],
});
// Extract tool calls and reasoning from response
const toolCalls: NliToolCall[] = [];
let reasoning = "";
for (const block of response.content) {
if (block.type === "text") {
reasoning += block.text;
} else if (block.type === "tool_use") {
toolCalls.push({
toolName: block.name,
input: block.input as Record<string, unknown>,
});
}
}
return { toolCalls, reasoning };
}