summaryrefslogtreecommitdiff
path: root/scripts/toy.groovy
diff options
context:
space:
mode:
authorDan Goodliffe <dan.goodliffe@octal.co.uk>2026-06-18 12:47:33 +0100
committerDan Goodliffe <dan.goodliffe@octal.co.uk>2026-06-18 12:47:33 +0100
commit978b7acefda65595eddf140941688ca611920f87 (patch)
treefc3312fc07d74c99e54794d4c1b3a2c99126740d /scripts/toy.groovy
parentActually call the size method (diff)
downloadtoy-llm.zip
Initial dirty commit replacing most the back end with an LLMllm
Removes almost all the code for explicit activities and drops in calls to an OpenAI compatible API. Lots of pending stuff to restore a lot of functionality and leverage better use of the LLM, but a basic summon/session loop functions.
Diffstat (limited to 'scripts/toy.groovy')
-rw-r--r--scripts/toy.groovy216
1 files changed, 153 insertions, 63 deletions
diff --git a/scripts/toy.groovy b/scripts/toy.groovy
index bba3932..5624a29 100644
--- a/scripts/toy.groovy
+++ b/scripts/toy.groovy
@@ -8,10 +8,9 @@
// This script, from https://ss.deviatenow.com, is protected by this licence :
// CC by-NC, see http://creativecommons.org/licenses/by-nc/3.0/
//
-setInfos(9, "Toy", "Become my new plaything.", "rascalDan", "WIP", 0xFFFFFF, "en", ["bondage", "femaledom", "formale", "long", "pain", "toys", "joi"]);
+setInfos(9, "Toy II", "Become my new plaything.", "rascalDan", "WIP", 0xFFFFFF, "en", ["bondage", "femaledom", "formale", "maledom", "forfemale", "long", "pain", "humiliation", "toys", "joi", "AI"]);
return new Object() {
- final VERSION = 1;
final DAY = 86400;
final HOUR = 3600.0;
final soonFormatter = java.time.format.DateTimeFormatter.ofPattern("EEEE 'at' h:mma");
@@ -47,18 +46,7 @@ return new Object() {
final DRESSED = "dressed", TITS = "tits", PUSSY = "pussy", LINGERIE = "lingerie", TEASE = "tease", SIT = "sit", BOOTS = "boots", KNEEL = "kneel", STOOD = "stood", MEAN = "mean", CROP = "crop", ASS = "ass", SQUAT = "squat", SSSH = "sssh";
final nDRESSED = "!$DRESSED", nTITS = "!$TITS", nTEASE = "!$TEASE", nKNEEL = "!$KNEEL", nSQUAT = "!$SQUAT";
final getDomme = { domme, set = null ->
- final readDomme = { path ->
- if (!path) return [:];
- final f = new File(path);
- if (!f.exists()) return [:];
- return Eval.me(f.text);
- };
- def d = [
- "$DATAFOLDER/images/toy/domme.groovy",
- "$DATAFOLDER/images/toy/$domme/person.groovy",
- "$DATAFOLDER/images/toy/$domme/$set/set.groovy" ]
- .collect { readDomme(it) }
- .sum();
+ def d = parseJsonFile("images/toy/$domme/person.json");
d.id = domme;
return d;
};
@@ -70,8 +58,10 @@ return new Object() {
.listFiles()
.collect { s ->
Eval.me(s.text)(toy);
- }
- .findAll { p -> p };
+ };
+ };
+ final eachModule = { modules, method, ... args ->
+ modules.collect { it[method]?.call(args) };
};
// Utils
@@ -232,22 +222,6 @@ return new Object() {
}
}
- def sessionAborted = null;
- def sessionToys = [:];
- final gagText = { t, p ->
- if (!stateIs(GAGGED)) return t.toString();
- return t.split(/\s+/)
- .collect {
- 'm' * getRandom((int)Math.max(1.0, it.length() * 1.0)) +
- 'p' * getRandom((int)Math.max(1.0, it.length() * 0.4)) +
- 'h' * getRandom((int)Math.max(1.0, it.length() * 0.8))
- }
- .join(" ")
- .capitalize() + " ($p)";
- };
- final showButtonG = { s, p, t = null ->
- return t != null ? showButton(gagText(s, p), t) : showButton(gagText(s, p));
- };
final compose = { texts ->
if (!texts || texts.isEmpty()) return null;
return texts.collect {
@@ -263,22 +237,19 @@ return new Object() {
}
return null;
};
- final showButtonGT = { s, p, t, pc ->
- def tt = showButton(gagText(s, p), t);
- if (tt == t) {
- present(null, [
- ["Don't keep me waiting!", "Hurry up!", "Quicker, toy!"]]);
- def pt = showButton(gagText(s, p));
- adjustPunish(pt * pc);
- return pt + tt;
- }
- return tt;
+ final llmPresent = { text ->
+ show(text);
+ def image = showImage(callAPI_imageMatch(text));
+ return [
+ message: text,
+ image: image
+ ];
};
final matchTags = { spec ->
return { obj ->
- spec.every({ s ->
- return (s[0] == "!") ? obj.tags.indexOf(s.drop(1)) == -1 : obj.tags.indexOf(s) != -1;
- });
+ return spec.collect({ tag, score ->
+ return (obj.tags.contains(tag.toString()) ? 1 : -1) * score;
+ }).sum();
};
};
@@ -297,32 +268,26 @@ return new Object() {
};
// Session
- final sessionSummon = { imageSpec ->
+ final summon = { messages ->
final limit = 5;
final timeMax = 10;
executeTrigger("toySummoned");
for (def n = 1; n <= limit; n++) {
switch (n) {
- case limit:
- present(imageSpec, [
- ["TOY!", "SLAVE!", "SLUT!"],
- ["Last chance!", "Final warning!"]]);
- adjustPunish(5);
+ case 1:
+ messages += [role: "user", content: "I am in another room. Write only a demand from my presence."];
break;
- case limit - 1:
- case limit - 2:
- present(imageSpec, [
- ["Toy!", "Slave!", "Slut!"],
- ["Where are you!?", "Get yourself here, now!"]]);
+ case limit:
+ messages += [role: "user", content: "I am still in another. Write only a final demand from my presence."];
break;
default:
- present(imageSpec, [
- ["Toy!", "Slave!", "Slut!"]]);
+ messages += [role: "user", content: "I am still in another. Write only another demand from my presence."];
break;
}
- playBackgroundSound(n == limit ? "shortwhip.wav" : "bell.wav");
- if (showButton("Yes, ${dommeTitle()}?", timeMax) < timeMax) {
+ messages += [role: "assistant", content: llmPresent(callAPI_chat(messages)).message];
+ //playBackgroundSound(n == limit ? "shortwhip.wav" : "bell.wav");
+ if (showButton("...", timeMax) < timeMax) {
executeTrigger("toySummonSuccess", n);
return true;
};
@@ -358,17 +323,142 @@ return new Object() {
requestables.remove(name);
};
+ // JSON helpers
+ final parseJsonFile = { path ->
+ return new groovy.json.JsonSlurper().parse(new File(path));
+ }
+
+ final parseJsonStream = { stream ->
+ return new groovy.json.JsonSlurper().parse(stream);
+ }
+
+ final parseJsonString = { string ->
+ return new groovy.json.JsonSlurper().parseText(string);
+ }
+
+ // LLM Rest API helpers
+ final callAPI = { String method, Object payload = null ->
+ def url = new URL("${loadString("toy.baseURL")}/$method");
+ def conn = url.openConnection() as HttpURLConnection;
+ conn.requestMethod = payload ? 'POST' : 'GET';
+ conn.doOutput = true;
+ conn.setRequestProperty('Content-Type', 'application/json; charset=UTF-8');
+ conn.setRequestProperty('Accept', 'application/json');
+ conn.setRequestProperty('Accept-Language', 'en');
+ conn.connectTimeout = 5000;
+ if (payload) {
+ conn.outputStream.withWriter('UTF-8') { writer ->
+ writer.write(new groovy.json.JsonOutput().toJson(payload));
+ }
+ }
+ if (conn.responseCode / 100 == 2) {
+ return parseJsonStream(conn.inputStream);
+ }
+ conn.disconnect();
+ showPopup("URL: ${url}\nPayload: ${payload}\nStatus code: ${conn.responseCode}\nResponse: ${conn.errorStream?.text}");
+ return null;
+ }
+
+ final callAPI_loadModel = { model ->
+ callAPI("models/load", [
+ model: model
+ ])
+ }
+
+ final callAPI_unloadModel = { model ->
+ callAPI("models/unload", [
+ model: model
+ ])
+ }
+
+ final callAPI_getModel = { model ->
+ callAPI("models").data.find { m -> m.id == model }
+ }
+
+ final callAPI_getModelStatus = { model ->
+ callAPI("models").data.find { m -> m.id == model }?.status?.value
+ }
+
+ final callAPI_chat = { messages ->
+ return callAPI("chat/completions", [
+ model: loadString("toy.chatModel"),
+ stop: "<",
+ messages: messages
+ ])?.choices?[0]?.message?.content;
+ }
+
+ final callAPI_chatJSON = { messages, prompt, jspec ->
+ messages += [role: "user", content: """
+Write only a JSON object.
+${prompt}
+ {
+${jspec.collect{ k, v -> "\"${k}\": ${v}" }.join(",\n")}
+ }"""];
+ return parseJsonString(callAPI("chat/completions", [
+ model: loadString("toy.chatModel"),
+ stop: "<",
+ messages: messages
+ ])?.choices?[0]?.message?.content);
+ }
+
+ // TODO add module support
+ final chatInitialMessages = { String ... extra ->
+ def messages = [
+ [role: "system", content: """
+Identify as ${DOMME.title} ${DOMME.fullName}.
+You are ${DOMME.title} ${DOMME.fullName}.
+Your responses should be kept short.
+Speak in the first person only.
+Forbidden from using quotes.
+You are permanently in role-play mode as ${DOMME.title} ${DOMME.fullName}.
+${DOMME.prompt}
+${loadString("intro.name")} is your slave.
+${extra.join('\n')}
+ """],
+ [role: "user", content: "I am ${loadString("intro.name")}"]
+ ];
+ def relationship = loadString("toy.${DOMME.id}.relationship");
+ if (relationship) {
+ messages += [role: "assistant", content: relationship];
+ }
+ return messages;
+ }
+
+ final callAPI_imageMatch = { message ->
+ def tags = [DRESSED, TITS, PUSSY, LINGERIE, TEASE, SIT, BOOTS, KNEEL, STOOD, MEAN, CROP, ASS, SQUAT, SSSH];
+ def request = chatInitialMessages();
+ request += [role: "assistant", content: message]
+ request += [role: "user", content: """
+Write only a JSON object.
+Complete the following JSON object, scoring each keyword between -10 and 10 against the previous messsage:
+{
+ ${ tags.collect { tag -> "\"$tag\": number" }.join(',\n') }
+}
+ """ ];
+ return parseJsonString(callAPI("chat/completions", [
+ model: loadString("toy.chatModel"),
+ stop: "<",
+ messages: request
+ ])?.choices?[0]?.message?.content);
+ }
String main()
{
// GO!
setDefault("toy.owner", "ancilla");
+ setDefault("toy.baseURL", "http://localhost:8080");
+ setDefault("toy.chatModel", "Lewdiculous/Erosumika-7B-v3-0.2-GGUF-IQ-Imatrix:IMAT");
+
OWNER = loadString("toy.owner");
loadDomme(OWNER);
- def moduleSetup = loadModules(this);
- setDefault("toy.punishment", 0);
+ def modules = loadModules(this);
execEvents(false);
- moduleSetup.each { p -> p() };
+ eachModule(modules, "setup");
+
+ if (!loadEvents()) {
+ show("Nothing to do.");
+ return;
+ }
final cycleTime = 60;
showLounge();