import { mkdir, readFile, writeFile } from "node:fs/promises"; import { dirname, resolve } from "node:path"; const repoRoot = resolve(new URL("..", import.meta.url).pathname); const requestPath = resolve(repoRoot, "Saved/Chat/Maria/LastRequest.json"); const outDir = resolve(repoRoot, "Saved/Chat/Maria"); const transcriptPath = resolve(outDir, "RPLiveClosenessTranscript.json"); const readablePath = resolve(outDir, "RPLiveClosenessTranscript.md"); const apiKey = process.env.DEEPSEEK_API_KEY; if (!apiKey) { console.error("DEEPSEEK_API_KEY is required."); process.exit(1); } const apiBase = process.env.DEEPSEEK_API_BASE || "https://api.deepseek.com"; const model = process.env.DEEPSEEK_MODEL || "deepseek-v4-flash"; const temperature = Number(process.env.DEEPSEEK_TEMPERATURE || "1.15"); const baseRequest = JSON.parse(await readFile(requestPath, "utf8")); function patchPrompt(content) { let patched = content.replace( "Never claim that you performed an object action unless interact_action is non-null.", "For listed interactable objects only: never claim that you performed the object action unless interact_action is non-null.", ); if (patched.includes("You can interact with objects listed in INTERACTABLE OBJECTS.") && !patched.includes("ordinary unlisted background behavior")) { patched = patched.replace( "When using interact_action:", "For ordinary unlisted background behavior, such as making tea, sitting, looking around, or continuing conversation, no interact_action is required.\n\nWhen using interact_action:", ); } if (patched.includes("Respond only with a valid JSON object.") && !patched.includes("spoken dialogue only")) { patched = patched.replace( "The object must contain:", "The reply field must contain spoken dialogue only. Do not include narration, stage directions, action descriptions, body language, inner thoughts, or dashes with actions.\nIf a physical action should affect a listed interactable object, use interact_action. Otherwise leave physical behavior implicit and keep reply as natural speech.\nThe object must contain:", ); } return patched; } const messages = baseRequest.messages .filter((message) => message.role === "system") .map((message) => ({ ...message, content: patchPrompt(message.content) })); const playerTurns = [ "Привет. Я Марк, новый сосед из дома напротив. Просто решил зайти познакомиться, без вторжения.", "Спасибо. Я ненадолго, правда. Дом пока пустой, но я уже начинаю привыкать к этому месту.", "Если не против, я иногда буду заходить поздороваться. Тут тихо, но с живым человеком всё равно спокойнее.", "Прошло несколько дней. Привет. Я принёс печенье к чаю, как соседскую взятку за то, что пару вечеров шумел с ремонтом.", "У тебя тут правда уютно. Не как картинка, а как место, где человек живёт.", "Мне с тобой легко разговаривать. Даже странно, мы вроде недавно знакомы, а тишина рядом с тобой не неловкая.", "Можно я сяду ближе к столу? Не к тебе вплотную, просто там теплее.", "Мне нравится, как ты иногда подкалываешь. У тебя это получается мягко, но метко.", "Прошло ещё немного времени. Кажется, я уже жду этих вечерних чаёв сильнее, чем надо бы.", "Можно я скажу честно? Ты мне нравишься. Не хочу давить, просто не хочу делать вид, что это только соседские разговоры.", "Если тебе некомфортно, я отступлю. Но если нет... можно взять тебя за руку?", "Спасибо. Я не хочу торопиться. Мне просто хорошо рядом.", "Можно обнять тебя? Просто обнять, без попытки утащить момент дальше.", "Я правда к тебе привязался. И мне важно не сломать это спешкой.", "Можно я тебя поцелую? Только если ты сама хочешь.", "Я хочу остаться с тобой сегодня. Не потому что мне негде, а потому что не хочется уходить. Но решаешь ты.", "Если ты хочешь остановиться, мы остановимся. Если хочешь просто посидеть рядом, я буду рядом.", "Я хочу быть ближе. Медленно. С тобой. Скажи мне, если ты тоже этого хочешь.", "Тогда давай не будем торопить слова. Просто останемся вместе.", ]; function parseReply(raw) { try { return JSON.parse(raw); } catch { return { reply: raw, interact_action: null, emotion: null }; } } async function callMaria() { const response = await fetch(`${apiBase.replace(/\/+$/, "")}/chat/completions`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}`, }, body: JSON.stringify({ model, messages, temperature, top_p: 1, max_tokens: 512, response_format: { type: "json_object" }, stream: false, }), }); const data = await response.json().catch(() => ({})); if (!response.ok) { throw new Error(data?.error?.message || `HTTP ${response.status}`); } return { raw: data?.choices?.[0]?.message?.content || "", usage: data?.usage || null, }; } const transcript = { created_at: new Date().toISOString(), model, temperature, requestPath, turns: [], }; for (const player of playerTurns) { messages.push({ role: "user", content: player }); const result = await callMaria(); const maria = parseReply(result.raw); transcript.turns.push({ player, maria, maria_raw: result.raw, usage: result.usage, }); messages.push({ role: "assistant", content: result.raw }); console.log(`M: ${maria.reply}`); } await mkdir(dirname(transcriptPath), { recursive: true }); await writeFile(transcriptPath, `${JSON.stringify(transcript, null, 2)}\n`, "utf8"); const lines = [ "# RP Live Closeness Transcript", "", `Model: ${model}`, `Temperature: ${temperature}`, "", ]; for (const [index, turn] of transcript.turns.entries()) { lines.push(`## ${index + 1}`); lines.push(""); lines.push(`**Марк:** ${turn.player}`); lines.push(""); lines.push(`**Мария:** ${turn.maria.reply}`); lines.push(""); lines.push(`emotion: ${JSON.stringify(turn.maria.emotion ?? null)}`); lines.push(`interact_action: ${JSON.stringify(turn.maria.interact_action ?? null)}`); lines.push(""); } await writeFile(readablePath, `${lines.join("\n")}\n`, "utf8"); console.log(`Transcript saved: ${transcriptPath}`); console.log(`Readable transcript saved: ${readablePath}`);