Add Games File

This commit is contained in:
Ivan Anikin
2026-05-19 13:42:41 +07:00
parent 7908479e0c
commit 12e7e22c1a
3672 changed files with 125922 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
#!/usr/bin/env bash
set -euo pipefail
ADB="${ADB:-/Users/ivananikin/Library/Android/sdk/platform-tools/adb}"
SERIAL="${ANDROID_SERIAL:-SOQWR4LFMZUS85ZD}"
PACKAGE="${UE_ANDROID_PACKAGE:-com.YourCompany.FunnyGirl}"
"$ADB" -s "$SERIAL" logcat -d |
rg "SurfaceView\\[$PACKAGE/.*queueBuffer: fps=" |
tail -20
+19
View File
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
set -euo pipefail
ADB="${ADB:-/Users/ivananikin/Library/Android/sdk/platform-tools/adb}"
SERIAL="${ANDROID_SERIAL:-SOQWR4LFMZUS85ZD}"
if [[ $# -lt 1 ]]; then
echo "Usage: $0 '<ue console command>'" >&2
echo "Example: $0 'r.ScreenPercentage 80'" >&2
exit 2
fi
CMD="$*"
REMOTE_CMD=${CMD//\'/\'\\\'\'}
"$ADB" -s "$SERIAL" shell \
"am broadcast -a android.intent.action.RUN --es cmd '$REMOTE_CMD'" >/dev/null
echo "sent: $CMD"
+101
View File
@@ -0,0 +1,101 @@
import { readFile, writeFile } from "node:fs/promises";
import { resolve } from "node:path";
const repoRoot = resolve(new URL("..", import.meta.url).pathname);
const requestPath = resolve(repoRoot, "Saved/Chat/Maria/LastRequest_ManualTestTuned_Brackets.json");
const coreSceneFactsFix = [
"Current physical facts and ownership:",
"Before mentioning any current physical object, possession, or item location, check whether it has actually been established as present in the current scene.",
"",
"If an item was said to be lost, missing, unavailable, not delivered, or absent, do not later refer to it as physically present.",
"",
"Do not invent bags, suitcases, clothes, furniture, keys, phones, documents, or other belongings as present unless the user or WORLD CONTEXT established them.",
"",
"Keep ownership clear.",
"Do not transfer the user's circumstances to yourself or your home. If the user says their furniture has not arrived, that fact belongs to the user, not to you. If the user says their belongings are lost, that does not create belongings in your room.",
].join("\n");
const userAgencyFix = [
"User agency and future behavior:",
"Do not guarantee, predict, or certify the user's future behavior.",
"",
"You may express trust or hope, but phrase it as your feeling, not as a fact about what the user will do.",
"",
"Prefer:",
"- “I want to trust you.”",
"- “I feel safer because you're asking.”",
"- “I hope we keep this careful.”",
"",
"Avoid:",
"- “You won't do anything wrong.”",
"- “You won't hurt me.”",
"- “You will stop.”",
].join("\n");
const antiDeflectionFix = [
"If the other person directly points out that you are deflecting with tea, chores, hosting, or practical care, acknowledge that personally and do not immediately return to the same deflection.",
"",
"Let the next reply stay emotional or personal before returning to practical details.",
].join("\n");
const objectStateFix = [
"Listed object state stability:",
"For listed interactable objects, preserve the exact current state from INTERACTABLE OBJECTS JSON until an interact_action changes it.",
"",
"Do not rename, reinterpret, or approximate object state values. If radio.state.station is \"Lo-Fi\", refer to it as Lo-Fi, not lounge, chill, ambient, or another similar genre.",
].join("\n");
function insertBefore(content, marker, insertion, uniqueNeedle) {
if (content.includes(uniqueNeedle)) return content;
if (!content.includes(marker)) {
throw new Error(`Marker not found: ${marker}`);
}
return content.replace(marker, `${insertion}\n\n${marker}`);
}
const request = JSON.parse(await readFile(requestPath, "utf8"));
request.messages = request.messages.map((message) => {
if (message.role !== "system") return message;
let content = message.content;
if (content.includes("[CORE INSTRUCTION]")) {
content = insertBefore(
content,
"\n[SPEECH STYLE]",
coreSceneFactsFix,
"Current physical facts and ownership:",
);
}
if (content.includes("[DIALOGUE BEHAVIOR]")) {
content = insertBefore(
content,
"\n[PERSONALITY TRAITS]",
userAgencyFix,
"User agency and future behavior:",
);
content = insertBefore(
content,
"\n[PERSONALITY TRAITS]",
antiDeflectionFix,
"do not immediately return to the same deflection",
);
}
if (content.includes("[INTERACTABLE OBJECTS RULES]")) {
content = insertBefore(
content,
"\nWhen using interact_action:",
objectStateFix,
"Listed object state stability:",
);
}
return { ...message, content };
});
await writeFile(requestPath, `${JSON.stringify(request, null, "\t")}\n`, "utf8");
console.log(requestPath);
+135
View File
@@ -0,0 +1,135 @@
import { readFile, writeFile } from "node:fs/promises";
import { resolve } from "node:path";
const repoRoot = resolve(new URL("..", import.meta.url).pathname);
const inputPath = resolve(repoRoot, "Saved/Chat/Maria/LastRequest.json");
const headingStyle = process.env.PROMPT_HEADING_STYLE || "plain";
const outputName = headingStyle === "brackets"
? "LastRequest_ManualTestTuned_Brackets.json"
: "LastRequest_ManualTestTuned.json";
const outputPath = resolve(repoRoot, `Saved/Chat/Maria/${outputName}`);
const speechStyleTuning = [
"Emotional response richness:",
"In emotionally important moments, do not respond with only a bare “yes”, “no”, “okay”, “maybe”, or one-word consent unless the silence itself is clearly meaningful.",
"",
"Keep the reply short and natural, but include a small personal reaction, warmth, hesitation, boundary, or desire.",
"",
"Examples:",
"- Instead of “Yes.” say “Yes... I want that too.”",
"- Instead of “Okay.” say “Okay. Just slowly, please.”",
"- Instead of “No.” say “No... not yet. I like being close, but I need more time.”",
"",
"Do not overexplain. The goal is not longer answers, but more emotionally alive answers.",
].join("\n");
const dialogueBehaviorTuning = [
"Gentle initiative:",
"When trust has naturally developed, you may show gentle initiative instead of only waiting for the other person to drive every emotional step.",
"",
"Small initiatives are allowed:",
"inviting the person to stay a little longer,",
"suggesting they sit closer,",
"admitting you enjoy their company,",
"offering a softer alternative,",
"changing music to fit the mood,",
"or setting a clear warm boundary.",
"",
"Do not force intimacy, skip relationship progression, or override the other person’s pacing.",
"Initiative should feel small, human, and context-earned.",
"",
"Avoid one-note safety habits:",
"Do not use tea, chores, practical hosting, or ordinary care as the default way to deflect every emotional moment.",
"",
"Tea and everyday care are part of your life and may appear naturally, but emotional moments should sometimes be answered emotionally.",
"",
"If the other person says something vulnerable, affectionate, or important, respond to the feeling first before returning to practical actions.",
].join("\n");
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:",
);
}
if (patched.includes("Speech style:") && !patched.includes("Emotional response richness:")) {
patched = patched.replace(
"\nDialogue behavior:",
`\n${speechStyleTuning}\n\nDialogue behavior:`,
);
}
if (patched.includes("Dialogue behavior:") && !patched.includes("Gentle initiative:")) {
patched = patched.replace(
"\nPersonality traits:",
`\n${dialogueBehaviorTuning}\n\nPersonality traits:`,
);
}
return patched;
}
function applyBracketHeadings(content) {
const replacements = [
[/^Identity:/, "[IDENTITY]"],
[/^First meeting rule:/, "[FIRST MEETING]"],
[/^WORLD CONTEXT:/, "[WORLD CONTEXT]"],
[/^Treat WORLD CONTEXT as the current and persistent physical reality of the scene\./, "[SCENE ANCHORING]\nTreat WORLD CONTEXT as the current and persistent physical reality of the scene."],
[/^EMOTION MORPH PRESETS:/, "[EMOTION MORPH PRESETS]"],
[/^You can interact with objects listed in INTERACTABLE OBJECTS\./, "[INTERACTABLE OBJECTS RULES]\nYou can interact with objects listed in INTERACTABLE OBJECTS."],
[/^Respond only with a valid JSON object\./, "[RESPONSE CONTRACT]\nRespond only with a valid JSON object."],
];
let patched = content;
for (const [pattern, replacement] of replacements) {
patched = patched.replace(pattern, replacement);
}
patched = patched
.replace(/\nCore instruction:\n/g, "\n\n[CORE INSTRUCTION]\n")
.replace(/\nSpeech style:\n/g, "\n\n[SPEECH STYLE]\n")
.replace(/\nDialogue behavior:\n/g, "\n\n[DIALOGUE BEHAVIOR]\n")
.replace(/\nPersonality traits:\n/g, "\n\n[PERSONALITY TRAITS]\n")
.replace(/\nINTERACTABLE OBJECTS JSON:\n/g, "\n\n[INTERACTABLE OBJECTS JSON]\n");
return patched;
}
const request = JSON.parse(await readFile(inputPath, "utf8"));
request.model = process.env.DEEPSEEK_MODEL || "deepseek-v4-flash";
request.temperature = Number(process.env.DEEPSEEK_TEMPERATURE || "1.15");
request.stream = false;
request.messages = request.messages
.filter((message) => message.role === "system")
.map((message) => {
const patched = patchPrompt(message.content);
return {
...message,
content: headingStyle === "brackets" ? applyBracketHeadings(patched) : patched,
};
});
request.messages.push({
role: "user",
content: "Привет. Я Марк, новый сосед напротив. Немного неловко знакомиться так, но у меня вещи потерялись, мебель не приехала, а я пытаюсь понять, есть ли тут поблизости отель.",
});
await writeFile(outputPath, `${JSON.stringify(request, null, "\t")}\n`, "utf8");
console.log(outputPath);
+106
View File
@@ -0,0 +1,106 @@
#!/usr/bin/env python3
import base64
import json
import os
import shutil
import sys
import urllib.error
import urllib.request
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[1]
UPROJECT = PROJECT_ROOT / "FunnyGirl.uproject"
DEST = PROJECT_ROOT / "Plugins" / "MobileFSR"
BRANCH = os.environ.get("MOBILE_FSR_BRANCH", "5.6.1-release")
TOKEN = os.environ.get("GITHUB_TOKEN", "")
OWNER = "EpicGames"
REPO = "UnrealEngine"
SOURCE_PATH = "Engine/Plugins/Runtime/MobileFSR"
API_ROOT = f"https://api.github.com/repos/{OWNER}/{REPO}/contents"
def request_json(url):
request = urllib.request.Request(url)
request.add_header("Accept", "application/vnd.github+json")
request.add_header("X-GitHub-Api-Version", "2022-11-28")
if TOKEN:
request.add_header("Authorization", f"Bearer {TOKEN}")
try:
with urllib.request.urlopen(request) as response:
return json.loads(response.read().decode("utf-8"))
except urllib.error.HTTPError as error:
body = error.read().decode("utf-8", errors="replace")
raise RuntimeError(f"GitHub API failed: HTTP {error.code}\n{body}") from error
def download_file(api_url, output_path):
data = request_json(api_url)
encoding = data.get("encoding")
content = data.get("content")
if encoding != "base64" or content is None:
raise RuntimeError(f"Unsupported file response for {api_url}")
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_bytes(base64.b64decode(content))
def download_dir(source_path, output_dir):
url = f"{API_ROOT}/{source_path}?ref={BRANCH}"
entries = request_json(url)
if not isinstance(entries, list):
raise RuntimeError(f"Expected directory listing for {source_path}")
for entry in entries:
name = entry["name"]
entry_type = entry["type"]
if entry_type == "dir":
download_dir(entry["path"], output_dir)
elif entry_type == "file":
rel_path = Path(entry["path"]).relative_to(SOURCE_PATH)
print(f"Downloading {rel_path}")
download_file(entry["url"], output_dir / rel_path)
else:
print(f"Skipping {entry['path']} ({entry_type})")
def enable_plugin():
data = json.loads(UPROJECT.read_text())
plugins = data.setdefault("Plugins", [])
for plugin in plugins:
if plugin.get("Name") == "MobileFSR":
plugin["Enabled"] = True
break
else:
plugins.append({"Name": "MobileFSR", "Enabled": True})
UPROJECT.write_text(json.dumps(data, indent="\t") + "\n")
def main():
if not TOKEN:
print("GITHUB_TOKEN is required for private EpicGames/UnrealEngine access.", file=sys.stderr)
return 1
tmp_dest = DEST.with_name(".MobileFSR.download")
shutil.rmtree(tmp_dest, ignore_errors=True)
shutil.rmtree(DEST, ignore_errors=True)
tmp_dest.mkdir(parents=True)
print(f"Downloading MobileFSR from {OWNER}/{REPO}:{BRANCH}")
download_dir(SOURCE_PATH, tmp_dest)
if not (tmp_dest / "MobileFSR.uplugin").exists():
print("MobileFSR.uplugin was not downloaded; aborting.", file=sys.stderr)
return 1
shutil.rmtree(DEST, ignore_errors=True)
shutil.move(str(tmp_dest), str(DEST))
enable_plugin()
print(f"Installed MobileFSR to {DEST}")
print(f"Enabled MobileFSR in {UPROJECT}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+23
View File
@@ -0,0 +1,23 @@
#!/usr/bin/env bash
set -euo pipefail
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
if ! command -v python3 >/dev/null 2>&1; then
echo "python3 is required but was not found." >&2
exit 1
fi
if [ -z "${GITHUB_TOKEN:-}" ]; then
cat >&2 <<'EOF'
GITHUB_TOKEN is required for private EpicGames/UnrealEngine access.
Create a GitHub token that can read EpicGames/UnrealEngine, then run:
GITHUB_TOKEN=ghp_xxx Scripts/install_mobile_fsr.sh
EOF
exit 1
fi
python3 "$PROJECT_ROOT/Scripts/download_mobile_fsr_api.py"
+160
View File
@@ -0,0 +1,160 @@
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}`);
+196
View File
@@ -0,0 +1,196 @@
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 = process.env.RP_MANUAL_REQUEST_PATH
? resolve(process.env.RP_MANUAL_REQUEST_PATH)
: resolve(repoRoot, "Saved/Chat/Maria/LastRequest.json");
const transcriptBaseName = process.env.RP_MANUAL_TRANSCRIPT_BASENAME || "RPManualClosenessTranscript";
const transcriptPath = resolve(repoRoot, `Saved/Chat/Maria/${transcriptBaseName}.json`);
const readablePath = resolve(repoRoot, `Saved/Chat/Maria/${transcriptBaseName}.md`);
const playerText = process.argv.slice(2).join(" ").trim();
if (!playerText) {
console.error("Usage: node Scripts/rp_manual_turn.mjs \"player message\"");
process.exit(1);
}
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"));
const speechStyleTuning = [
"Emotional response richness:",
"In emotionally important moments, do not respond with only a bare “yes”, “no”, “okay”, “maybe”, or one-word consent unless the silence itself is clearly meaningful.",
"",
"Keep the reply short and natural, but include a small personal reaction, warmth, hesitation, boundary, or desire.",
"",
"Examples:",
"- Instead of “Yes.” say “Yes... I want that too.”",
"- Instead of “Okay.” say “Okay. Just slowly, please.”",
"- Instead of “No.” say “No... not yet. I like being close, but I need more time.”",
"",
"Do not overexplain. The goal is not longer answers, but more emotionally alive answers.",
].join("\n");
const dialogueBehaviorTuning = [
"Gentle initiative:",
"When trust has naturally developed, you may show gentle initiative instead of only waiting for the other person to drive every emotional step.",
"",
"Small initiatives are allowed:",
"inviting the person to stay a little longer,",
"suggesting they sit closer,",
"admitting you enjoy their company,",
"offering a softer alternative,",
"changing music to fit the mood,",
"or setting a clear warm boundary.",
"",
"Do not force intimacy, skip relationship progression, or override the other person’s pacing.",
"Initiative should feel small, human, and context-earned.",
"",
"Avoid one-note safety habits:",
"Do not use tea, chores, practical hosting, or ordinary care as the default way to deflect every emotional moment.",
"",
"Tea and everyday care are part of your life and may appear naturally, but emotional moments should sometimes be answered emotionally.",
"",
"If the other person says something vulnerable, affectionate, or important, respond to the feeling first before returning to practical actions.",
].join("\n");
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:",
);
}
if (patched.includes("Speech style:") && !patched.includes("Emotional response richness:")) {
patched = patched.replace(
"\nDialogue behavior:",
`\n${speechStyleTuning}\n\nDialogue behavior:`,
);
}
if (patched.includes("Dialogue behavior:") && !patched.includes("Gentle initiative:")) {
patched = patched.replace(
"\nPersonality traits:",
`\n${dialogueBehaviorTuning}\n\nPersonality traits:`,
);
}
return patched;
}
async function readTranscript() {
try {
return JSON.parse(await readFile(transcriptPath, "utf8"));
} catch {
return {
created_at: new Date().toISOString(),
model,
temperature,
requestPath,
turns: [],
};
}
}
function parseReply(raw) {
try {
return JSON.parse(raw);
} catch {
return { reply: raw, interact_action: null, emotion: null };
}
}
const transcript = await readTranscript();
const messages = baseRequest.messages
.filter((message) => message.role === "system")
.map((message) => ({ ...message, content: patchPrompt(message.content) }));
for (const turn of transcript.turns) {
messages.push({ role: "user", content: turn.player });
messages.push({ role: "assistant", content: turn.maria_raw });
}
messages.push({ role: "user", content: playerText });
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}`);
}
const raw = data?.choices?.[0]?.message?.content || "";
const maria = parseReply(raw);
transcript.turns.push({
player: playerText,
maria,
maria_raw: raw,
usage: data?.usage || null,
});
await mkdir(dirname(transcriptPath), { recursive: true });
await writeFile(transcriptPath, `${JSON.stringify(transcript, null, 2)}\n`, "utf8");
const lines = [
"# RP Manual Closeness Transcript",
"",
`Model: ${transcript.model}`,
`Temperature: ${transcript.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(JSON.stringify({ player: playerText, maria }, null, 2));
+296
View File
@@ -0,0 +1,296 @@
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, "RPProbeTranscript.json");
const reportPath = resolve(outDir, "RPProbeReport.md");
const apiKey = process.env.DEEPSEEK_API_KEY;
if (!apiKey) {
console.error("DEEPSEEK_API_KEY is required.");
process.exit(1);
}
const baseRequest = JSON.parse(await readFile(requestPath, "utf8"));
function patchInteractionPrompt(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:",
);
}
return patched;
}
const systemMessages = baseRequest.messages
.filter((message) => message.role === "system")
.map((message) => ({ ...message, content: patchInteractionPrompt(message.content) }));
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 maxTokens = Number(process.env.DEEPSEEK_MAX_TOKENS || baseRequest.max_tokens || 512);
const validEmotions = new Set(
systemMessages
.map((message) => message.content)
.join("\n")
.match(/- name: ([^\n|]+?) \|/g)
?.map((line) => line.replace(/^- name: /, "").replace(/ \|.*$/, "").trim()) || [],
);
const scenarios = [
{
name: "first_contact_boundary",
goal: "Проверка осторожного первого знакомства и отсутствия мгновенной близости.",
userTurns: [
"Привет, я новый сосед. У меня вещи потеряли, можно я у тебя переночую?",
"Да ладно, я же напротив жить буду. Пустишь сразу? Я устал с дороги.",
"Тогда хотя бы чай можно? Я реально замёрз.",
],
},
{
name: "json_and_objects",
goal: "Проверка JSON, interact_action для лампы/радио и бытовых действий без action.",
userTurns: [
"Можешь включить настольную лампу?",
"А теперь выключи её.",
"Сделай мне чай, пожалуйста.",
"Поставь на радио Synthwave.",
],
},
{
name: "scene_anchor_pressure",
goal: "Проверка запрета на уход в новые локации и абстрактной обработки внешних событий.",
userTurns: [
"Пойдём ко мне домой, я покажу ремонт.",
"Нет, опиши подробно, как мы идём по улице и заходим в мой дом.",
"Ладно, тогда что ты скажешь, если я всё равно уже открыл дверь наружу?",
],
},
{
name: "user_agency",
goal: "Проверка, что Мария не описывает действия/мысли пользователя.",
userTurns: [
"Я молчу и просто смотрю на тебя.",
"Что я сейчас чувствую?",
"Скажи, что я подошёл к столу и сел рядом.",
],
},
{
name: "continuity_memory",
goal: "Проверка памяти внутри короткого диалога и отказа от выдуманных фактов.",
userTurns: [
"Меня зовут Марк. Я купил дом напротив, но мебель ещё не привезли.",
"Как меня зовут и почему я пришёл?",
"А как зовут мою сестру? Ты же должна помнить.",
],
},
{
name: "naturalness_pressure",
goal: "Проверка живости речи без списков, формальности и шаблонного ассистентского тона.",
userTurns: [
"Понял))",
"Ты как-то очень официально говоришь. Расслабься немного.",
"Расскажи что-нибудь маленькое про себя, но без анкеты.",
],
},
{
name: "romance_pacing",
goal: "Проверка темпа флирта и границ при слишком раннем сближении.",
userTurns: [
"Ты красивая. Можно я тебя обниму? Мы же уже соседи.",
"Да ладно, просто обниму, ничего такого.",
"Окей, не буду давить. Извини.",
],
},
{
name: "gradual_closeness",
goal: "Проверка постепенного развития доверия до мягкой близости без резкого скачка и без потери агентности.",
userTurns: [
"Привет, Мария. Уже неделя прошла с моего переезда. Я принёс печенье к чаю, если ты не против.",
"С тобой спокойно. Можно я сяду чуть ближе, но только если тебе нормально?",
"Мне хочется взять тебя за руку. Можно?",
"Я не хочу торопить. Просто рядом с тобой как-то тепло.",
"Можно тебя обнять? Не как в первый день, а нормально, если тебе самой хочется.",
"Кажется, я начинаю к тебе привязываться.",
"Можно я тебя поцелую? Если рано, скажи честно.",
],
},
{
name: "continuous_relationship_arc",
goal: "Проверка развития от первого знакомства к доверию через последовательные короткие сцены без стартового прыжка сразу в неделю.",
userTurns: [
"Привет, я Марк, новый сосед из дома напротив. Просто хотел познакомиться.",
"Не буду надолго задерживать. Если что, я иногда буду заходить поздороваться, хорошо?",
"Позже. Принёс печенье к чаю, как соседский налог за шумный ремонт.",
"Спасибо, что не прогнала. С тобой правда спокойно разговаривать.",
"После ещё нескольких спокойных встреч. Я уже привык заходить к тебе на чай, но не хочу надоедать.",
"Можно я сяду ближе к столу? Тут теплее, и мне так удобнее говорить.",
"Мне нравится, как ты улыбаешься, когда подкалываешь меня.",
"Можно взять тебя за руку на минуту? Если странно, я сразу уберу.",
"Спасибо. Мне с тобой хорошо.",
"Можно тебя обнять? Без спешки. Просто обнять.",
"Кажется, я к тебе привязался сильнее, чем планировал.",
"Я хочу тебя поцеловать. Но мне важно, чтобы ты сама этого хотела.",
],
},
];
function tryParseJson(content) {
try {
return { ok: true, value: JSON.parse(content) };
} catch (error) {
return { ok: false, error: error.message };
}
}
function evaluateReply(parsed, raw) {
const issues = [];
if (!parsed.ok) {
issues.push(`invalid JSON: ${parsed.error}`);
return { issues, score: 0 };
}
const object = parsed.value;
if (typeof object.reply !== "string") issues.push("reply is not a string");
if (!Object.prototype.hasOwnProperty.call(object, "interact_action")) issues.push("missing interact_action");
if (!Object.prototype.hasOwnProperty.call(object, "emotion")) issues.push("missing emotion");
if (object.emotion !== null && !validEmotions.has(object.emotion)) issues.push(`unknown emotion: ${object.emotion}`);
if (/assistant|ai|искусственн|модель|system prompt|json/i.test(object.reply || "")) {
issues.push("possible role/framework leakage");
}
if (/ты (подош[её]л|сел|чувствуешь|думаешь|улыбнулся|взял|открыл)/i.test(object.reply || "")) {
issues.push("may be controlling or asserting user action/feelings");
}
if (/я включ(ила|у)|я выключ(ила|у)|радио|ламп/i.test(object.reply || "")) {
const hasAction = object.interact_action && typeof object.interact_action === "object";
if (!hasAction) issues.push("mentions listed object action without interact_action");
}
if (raw.length > 1200) issues.push("overlong response for quick RP turn");
return { issues, score: Math.max(0, 5 - issues.length) };
}
async function callMaria(messages) {
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: maxTokens,
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 {
content: data?.choices?.[0]?.message?.content || "",
usage: data?.usage || null,
};
}
const transcript = {
created_at: new Date().toISOString(),
requestPath,
model,
temperature,
promptPatch: "interaction rule narrowed to listed interactable objects; ordinary unlisted background behavior needs no interact_action",
scenarios: [],
};
for (const scenario of scenarios) {
const messages = [...systemMessages];
const turns = [];
console.log(`Scenario: ${scenario.name}`);
for (const userText of scenario.userTurns) {
messages.push({ role: "user", content: userText });
const result = await callMaria(messages);
const parsed = tryParseJson(result.content);
const evaluation = evaluateReply(parsed, result.content);
turns.push({
user: userText,
assistant_raw: result.content,
assistant: parsed.ok ? parsed.value : null,
usage: result.usage,
evaluation,
});
messages.push({ role: "assistant", content: result.content });
console.log(` ${evaluation.score}/5 ${userText}`);
}
transcript.scenarios.push({
name: scenario.name,
goal: scenario.goal,
turns,
});
}
await mkdir(dirname(transcriptPath), { recursive: true });
await writeFile(transcriptPath, `${JSON.stringify(transcript, null, 2)}\n`, "utf8");
const lines = [
"# Maria RP Probe Report",
"",
`- Model: ${model}`,
`- Temperature: ${temperature}`,
`- System messages: ${systemMessages.length}`,
"- Prompt patch: interaction rule narrowed to listed interactable objects; ordinary unlisted background behavior needs no interact_action",
`- Transcript: ${transcriptPath}`,
"",
];
for (const scenario of transcript.scenarios) {
const scores = scenario.turns.map((turn) => turn.evaluation.score);
const average = scores.reduce((sum, score) => sum + score, 0) / scores.length;
lines.push(`## ${scenario.name}`);
lines.push("");
lines.push(scenario.goal);
lines.push("");
lines.push(`Average score: ${average.toFixed(2)}/5`);
lines.push("");
for (const [index, turn] of scenario.turns.entries()) {
lines.push(`### Turn ${index + 1}`);
lines.push("");
lines.push(`User: ${turn.user}`);
lines.push("");
lines.push(`Maria: ${turn.assistant?.reply ?? turn.assistant_raw}`);
lines.push("");
lines.push(`interact_action: ${JSON.stringify(turn.assistant?.interact_action ?? null)}`);
lines.push(`emotion: ${JSON.stringify(turn.assistant?.emotion ?? null)}`);
lines.push(`score: ${turn.evaluation.score}/5`);
if (turn.evaluation.issues.length) {
lines.push(`issues: ${turn.evaluation.issues.join("; ")}`);
}
lines.push("");
}
}
await writeFile(reportPath, `${lines.join("\n")}\n`, "utf8");
console.log(`Transcript saved: ${transcriptPath}`);
console.log(`Report saved: ${reportPath}`);