Fix UI
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
@@ -34,6 +34,15 @@
|
||||
{
|
||||
"Name": "QwenTTSStreaming",
|
||||
"Enabled": true
|
||||
},
|
||||
{
|
||||
"Name": "RelativeIKOp",
|
||||
"Enabled": true
|
||||
},
|
||||
{
|
||||
"Name": "Nwiro",
|
||||
"Enabled": true,
|
||||
"MarketplaceURL": "com.epicgames.launcher://ue/Fab/product/22e3bc90-ed91-4154-8bea-21c33e6a51b7"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,160 +0,0 @@
|
||||
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}`);
|
||||
@@ -1,196 +0,0 @@
|
||||
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));
|
||||
@@ -1,296 +0,0 @@
|
||||
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}`);
|
||||
@@ -12,12 +12,12 @@ DEFINE_LOG_CATEGORY_STATIC(LogMorphController, Log, All);
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr float MinMorphTargetValue = -1.0f;
|
||||
constexpr float MaxMorphTargetValue = 1.0f;
|
||||
constexpr float MorphControllerMinMorphTargetValue = -1.0f;
|
||||
constexpr float MorphControllerMaxMorphTargetValue = 1.0f;
|
||||
|
||||
float ClampMorphTargetValue(float Value)
|
||||
{
|
||||
return FMath::Clamp(Value, MinMorphTargetValue, MaxMorphTargetValue);
|
||||
return FMath::Clamp(Value, MorphControllerMinMorphTargetValue, MorphControllerMaxMorphTargetValue);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr float MinMorphTargetValue = -1.0f;
|
||||
constexpr float MaxMorphTargetValue = 1.0f;
|
||||
constexpr float MorphPresetMinMorphTargetValue = -1.0f;
|
||||
constexpr float MorphPresetMaxMorphTargetValue = 1.0f;
|
||||
|
||||
float EvaluateTimelineClipCurve(const FMorphTimelineClip& Clip, float Time)
|
||||
{
|
||||
@@ -90,7 +90,7 @@ void MorphPresetRuntime::AccumulatePresetMorphValuesAtTime(
|
||||
continue;
|
||||
}
|
||||
|
||||
const float Contribution = FMath::Clamp(EvaluateClipWeight(Clip, Time) * PresetWeight, MinMorphTargetValue, MaxMorphTargetValue);
|
||||
const float Contribution = FMath::Clamp(EvaluateClipWeight(Clip, Time) * PresetWeight, MorphPresetMinMorphTargetValue, MorphPresetMaxMorphTargetValue);
|
||||
if (FMath::Abs(Contribution) < 0.001f)
|
||||
{
|
||||
continue;
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr float MinMorphTargetValue = -1.0f;
|
||||
constexpr float MaxMorphTargetValue = 1.0f;
|
||||
constexpr float MorphTargetLibraryMinMorphTargetValue = -1.0f;
|
||||
constexpr float MorphTargetLibraryMaxMorphTargetValue = 1.0f;
|
||||
}
|
||||
|
||||
TArray<FString> UMorphTargetBlueprintLibrary::GetMorphTargetNames(USkeletalMeshComponent* SkeletalMeshComponent)
|
||||
@@ -116,7 +116,7 @@ void UMorphTargetBlueprintLibrary::ApplyPresetToMeshAtTime(USkeletalMeshComponen
|
||||
|
||||
for (const auto& Pair : FinalValues)
|
||||
{
|
||||
SkeletalMeshComponent->SetMorphTarget(Pair.Key, FMath::Clamp(Pair.Value, MinMorphTargetValue, MaxMorphTargetValue), false);
|
||||
SkeletalMeshComponent->SetMorphTarget(Pair.Key, FMath::Clamp(Pair.Value, MorphTargetLibraryMinMorphTargetValue, MorphTargetLibraryMaxMorphTargetValue), false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user