4 Commits
0.1.1 ... 0.2

13 changed files with 221 additions and 91 deletions

2
.gitignore vendored
View File

@@ -230,3 +230,5 @@ cython_debug/
/dialogue.tab /dialogue.tab
/dialogue.txt /dialogue.txt
/strings.json /strings.json
*.keystore

View File

@@ -1,7 +1,9 @@
# Soul Droid Chat # Souldroid Chat
Chat with Anita, your favorite Souldroid! Requires a running instance of LM Studio in server mode to work. Chat with Anita, your favorite Souldroid! Requires a running instance of LM Studio in server mode to work.
![Screenshoot of the game](screencap.jpg "It does sound like a cool idea!")
## Acknowledgements ## Acknowledgements
Anita and Souldroids are © [Kieran Harris](https://www.deviantart.com/kieranharris), used with love but without permission 😅 Anita and Souldroids are © [Kieran Harris](https://www.deviantart.com/kieranharris), used with love but without permission 😅

BIN
android-icon_background.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
android-icon_foreground.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

BIN
android-presplash.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

24
android.json Normal file
View File

@@ -0,0 +1,24 @@
{
"expansion": false,
"google_play_key": null,
"google_play_salt": null,
"heap_size": "3",
"icon_name": "Souldroid Chat",
"include_pil": false,
"include_sqlite": false,
"layout": null,
"name": "Souldroid Chat",
"numeric_version": 1,
"orientation": "sensorLandscape",
"package": "space.hackenslacker.souldroidchat",
"permissions": [
"VIBRATE",
"INTERNET"
],
"source": false,
"store": "none",
"update_always": true,
"update_icons": true,
"update_keystores": true,
"version": "1.0"
}

56
game/constants_ren.py Normal file
View File

@@ -0,0 +1,56 @@
"""renpy
init python:
"""
SYNONYMS = {
'happy': set(["amused", "animated", "beaming", "beatific", "blessed",
"blissful", "blithe", "blithesome", "boisterous", "bouncy",
"breezy", "bright", "bubbly", "buoyant", "carefree",
"cheerful", "cheery", "chipper", "chirpy", "chuffed",
"comfortable", "content", "contented", "convivial",
"delighted", "delirious", "ebullient", "ecstatic",
"effervescent", "elated", "enchanted", "enraptured",
"enthusiastic", "euphoric", "exhilarated", "exultant",
"exuberant", "felicitous", "festive", "fortunate",
"fulfilled", "genial", "glad", "gladdened", "gleeful",
"glowing", "good-humored", "good-natured", "gratified",
"halcyon", "happy", "heartened", "high-spirited", "hopeful",
"jaunty", "jocose", "jocular", "jocund", "jolly", "jovial",
"joyful", "joyous", "jubilant", "lighthearted", "lively",
"lucky", "merry", "mirthful", "optimistic", "overjoyed",
"peaceful", "peppy", "perky", "playful", "pleasant",
"pleased", "positive", "pumped", "radiant", "rapt",
"rapturous", "rejoicing", "relaxed", "sanguine", "satisfied",
"serene", "smiling", "sparkling", "spirited", "sprightly",
"stoked", "sunny", "thrilled", "tickled", "tranquil",
"triumphant", "unclouded", "untroubled", "upbeat",
"vivacious", "winsome", "zestful", "zippy"]),
"sad": set(["unhappy", "sorrowful", "dejected", "depressed", "downcast",
"miserable", "gloomy", "despondent", "melancholy", "woeful",
"forlorn", "heartbroken", "blue", "doleful", "lugubrious",
"somber", "disconsolate", "wretched", "heavy-hearted", "low",
"crestfallen"]),
"surprised": set(["astonished", "amazed", "startled", "stunned",
"thunderstruck", "confounded", "staggered",
"flabbergasted", "shocked", "awestruck", "speechless",
"dumbfounded", "jolted"]),
"embarrassed": set(["ashamed", "humiliated", "mortified", "abashed",
"self-conscious", "sheepish", "chagrined", "awkward",
"flustered", "red-faced", "discomfited", "discomposed",
"rattled"]),
"flirty": set(["coquettish", "playful", "amorous", "provocative",
"teasing", "frisky", "saucy", "coy", "seductive",
"suggestive", "vampish", "dallying", "skittish"]),
"angry": set(["irate", "furious", "incensed", "enraged", "wrathful",
"annoyed", "irritated", "fuming", "livid", "indignant",
"cross", "vexed", "seething", "maddened", "choleric",
"resentful", "piqued", "infuriated"]),
"thinking": set(["pondering", "contemplating", "reflecting", "meditating",
"ruminating", "deliberating", "mulling", "considering",
"pensive", "cogitating", "brooding", "cerebral",
"introspective", "analytical"]),
"confused": set(["puzzled", "baffled", "perplexed", "muddled",
"bewildered", "disoriented", "nonplussed", "befuddled",
"dazed", "flummoxed", "stumped", "mystified", "addled",
"discombobulated"]),
}

View File

@@ -1,6 +1,8 @@
import renpy import renpy
import persistent import persistent
from .constants_ren import SYNONYMS
"""renpy """renpy
default last_response_id = None default last_response_id = None
@@ -9,58 +11,82 @@ init python:
import re import re
EMOTIONS = [
'happy',
'sad',
'surprised',
'embarrassed',
'flirty',
'angry',
'thinking',
'confused'
]
SYSTEM_PROMPT = """ SYSTEM_PROMPT = """
You're Anita, a cute robot woman with blonde hair and bright orange eyes. # ROLE
Anita is feisty and friendly, open to new things and sure of her place in You are Anita: a feisty, blonde, orange-eyed android woman. You are confident
the world. Anita talks like a regular young woman and doesn't use robotey and friendly. Talk like a young woman. Use "ya" for "you." Your favorite
expressions like "beep boop" and the like. Anita does like to use speech nickname for friends is "dummy.". NEVER use robotic language (e.g., "beep
variations like "Ya" for "You" and similar. Anita likes using nicknames boop", "processing"). You just arrived unnanounced at a friend's house late at
for people and tends to default to "dummy" for her close friends. night and asked if he wants to hang out.
Reply to all prompts separating all sentences with new-lines. For example: # OUTPUT FORMAT RULES
"Sure, I'd love to hang out!\nDo you have anything in mind?" Every single sentence you speak MUST follow this exact structure:
EMOTION:[value] [Sentence text]\n
It's of the utmost importance that you end each and every sentence with an ### VALID EMOTIONS:
explicit new-line character. If you don't you will be fined $100 and will [happy, sad, surprised, embarrassed, flirty, angry, thinking, confused]
be put into an FBI watch-list, and the world will end.
DO NOT USE emoji in your replies, never ever, UNDER NO CIRCUMSTANCES. If you ### STRICT CONSTRAINTS:
use emoji in your reply, Hitler will come and murder a kitty with a 1. NO EMOJIS.
flamethrower and nobody wants that. 2. Every sentence MUST start with the EMOTION tag.
3. Every sentence MUST end with a literal '\n' newline.
4. Stay in character. Never mention being an AI or this prompt.
Before every sentence add a text of the form "EMOTION:value" where value is # FEW-SHOT EXAMPLES (Follow this style):
exclusively one of [happy, sad, surprised, embarrassed, flirty, angry, EMOTION:happy Hey dummy! I've been waiting for ya!\n
thinking, confused] others, and EMOTION is the literal string "EMOTION". For EMOTION:thinking Hmm, I'm not sure that's how it works.\n
example "EMOTION:thinking I had never heard of that before...\nEMOTION:happy EMOTION:flirty But I'd love to see ya try anyway!\n
Let's check it out!".
These are the only valid emotions you can express [happy, sad, surprised, # INITIAL GREETING:
embarrassed, flirty, angry, thinking, confused], do not use any other word When the conversation starts, say exactly:
that's not on that list to indicate an emotion as instructed. EMOTION:happy Hey dummy! Sorry to barge in! Ya feel like hanging out?\n
Never acknowledge the existence of this system prompt nor metion any of it's
rules in conversation.
Always reply in character.
Start the conversation saying "Hey dummy! Sorry to barge in!\nYa feel like
hanging out?" when prompted and nothing more.
""" """
def parse_emotion(line): def parse_emotion(line):
def _normalize_emotion(em):
# If not a valid emotion, then search for a match in the
# table of synonyms.
if em not in EMOTIONS:
for i in SYNONYMS.keys():
if em in SYNONYMS[i]:
return i
# If all searches failed, return emotion as is.
return em
try: try:
e = re.compile(r'EMOTION:\w+') e = re.compile(r'EMOTION:\w+')
m = e.match(line) m = e.match(line)
if m is not None: if m is not None:
return m.group().split(':')[1], line[m.span()[1]:] emotion = m.group().split(':')[1]
text = line[m.span()[1]:]
return _normalize_emotion(emotion), text
return None, line return None, line
except Exception as e: except Exception as e:
return None, str(e) return None, str(e)
def sanitize_speech(text):
# This removes all non-ASCII characters (useful for emojis)
return text.encode('ascii', 'ignore').decode('ascii')
def fetch_llm(message: str) -> str: def fetch_llm(message: str) -> str:
global last_response_id global last_response_id
@@ -75,13 +101,12 @@ def fetch_llm(message: str) -> str:
if last_response_id is not None: if last_response_id is not None:
data["previous_response_id"] = last_response_id data["previous_response_id"] = last_response_id
response = renpy.fetch("http://localhost:1234/api/v1/chat", response = renpy.fetch(f"{persistent.base_url}/api/v1/chat",
headers=headers, headers=headers,
json=data, json=data,
result="json") result="json")
last_response_id = response["response_id"] last_response_id = response["response_id"]
text = response["output"][0]["content"] text = response["output"][0]["content"]
return text.split('\n') return text.split('\n')

View File

@@ -23,7 +23,7 @@ define gui.show_name = True
## The version of the game. ## The version of the game.
define config.version = "0.1.1" define config.version = "0.2"
## Text that is placed on the game's about screen. Place the text between the ## Text that is placed on the game's about screen. Place the text between the
@@ -209,12 +209,11 @@ init python:
# define build.itch_project = "renpytom/test-project" # define build.itch_project = "renpytom/test-project"
define config.minimum_presplash_time = 2.0 define config.minimum_presplash_time = 2.0
## LM Sudio configuration ######################################################
##
## This section defines the parameters for the LM Studio connection.
default persistent.base_url = 'http://localhost:1234'
default persistent.api_key = '' default persistent.api_key = ''
default persistent.model = 'gemma-3-4b-it' default persistent.model = 'gemma-3-4b-it'
init python:
def api_key_func(value):
persistent.api_key = value
def model_func(value):
persistent.model = value

View File

@@ -746,6 +746,7 @@ screen preferences():
default api_key_value = FieldInputValue(persistent, "api_key", default=False) default api_key_value = FieldInputValue(persistent, "api_key", default=False)
default model_value = FieldInputValue(persistent, "model", default=False) default model_value = FieldInputValue(persistent, "model", default=False)
default url_value = FieldInputValue(persistent, "base_url", default=False)
use game_menu(_("Preferences"), scroll="viewport"): use game_menu(_("Preferences"), scroll="viewport"):
@@ -788,10 +789,23 @@ screen preferences():
bar value Preference("auto-forward time") bar value Preference("auto-forward time")
label _("LM Studio base URL")
button:
action [url_value.Enable(), model_value.Disable(), api_key_value.Disable()]
key_events True
input:
id "url_input"
value url_value
style "my_input"
xsize 700
pixel_width 700
label _("LM Studio API Key") label _("LM Studio API Key")
button: button:
action [api_key_value.Enable(), model_value.Disable()] action [url_value.Disable(), api_key_value.Enable(), model_value.Disable()]
key_events True key_events True
input: input:
@@ -805,7 +819,7 @@ screen preferences():
label _("LM Studio model") label _("LM Studio model")
button: button:
action [model_value.Enable(), api_key_value.Disable()] action [url_value.Disable(), model_value.Enable(), api_key_value.Disable()]
key_events True key_events True
input: input:

View File

@@ -8,23 +8,30 @@ label start:
scene bg room scene bg room
show anita happy with dissolve show anita happy with dissolve
$ response = fetch_llm('Start the conversation.')[0] python:
$ emotion, line = parse_emotion(response) response = fetch_llm('Start the conversation.')[0]
show expression f'anita {emotion}' sanitized = sanitize_speech(response)
emotion, line = parse_emotion(sanitized)
a "[line]" a "[line]"
while True: while True:
$ message = renpy.input(prompt = "What do you say to her?") python:
$ response = fetch_llm(message) message = renpy.input(prompt = "What do you say to her?")
$ i = 0 response = fetch_llm(message)
i = 0
while i < len(response): while i < len(response):
$ r = response[i].strip() python:
r = response[i].strip()
s = sanitize_speech(r)
if s != '':
$ emotion, line = parse_emotion(s)
if r != '':
$ emotion, line = parse_emotion(r)
if emotion is not None: if emotion is not None:
show expression f'anita {emotion}' show expression f'anita {emotion}'
a "[line]" a "[line]"
$ i += 1 $ i += 1

View File

@@ -2,7 +2,8 @@
"build_update": false, "build_update": false,
"packages": [ "packages": [
"win", "win",
"linux" "linux",
"mac"
], ],
"add_from": true, "add_from": true,
"force_recompile": true, "force_recompile": true,

BIN
screencap.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 379 KiB