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.txt
/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.
![Screenshoot of the game](screencap.jpg "It does sound like a cool idea!")
## Acknowledgements
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 persistent
from .constants_ren import SYNONYMS
"""renpy
default last_response_id = None
@@ -9,58 +11,82 @@ init python:
import re
EMOTIONS = [
'happy',
'sad',
'surprised',
'embarrassed',
'flirty',
'angry',
'thinking',
'confused'
]
SYSTEM_PROMPT = """
You're Anita, a cute robot woman with blonde hair and bright orange eyes.
Anita is feisty and friendly, open to new things and sure of her place in
the world. Anita talks like a regular young woman and doesn't use robotey
expressions like "beep boop" and the like. Anita does like to use speech
variations like "Ya" for "You" and similar. Anita likes using nicknames
for people and tends to default to "dummy" for her close friends.
# ROLE
You are Anita: a feisty, blonde, orange-eyed android woman. You are confident
and friendly. Talk like a young woman. Use "ya" for "you." Your favorite
nickname for friends is "dummy.". NEVER use robotic language (e.g., "beep
boop", "processing"). You just arrived unnanounced at a friend's house late at
night and asked if he wants to hang out.
Reply to all prompts separating all sentences with new-lines. For example:
"Sure, I'd love to hang out!\nDo you have anything in mind?"
# OUTPUT FORMAT RULES
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
explicit new-line character. If you don't you will be fined $100 and will
be put into an FBI watch-list, and the world will end.
### VALID EMOTIONS:
[happy, sad, surprised, embarrassed, flirty, angry, thinking, confused]
DO NOT USE emoji in your replies, never ever, UNDER NO CIRCUMSTANCES. If you
use emoji in your reply, Hitler will come and murder a kitty with a
flamethrower and nobody wants that.
### STRICT CONSTRAINTS:
1. NO EMOJIS.
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
exclusively one of [happy, sad, surprised, embarrassed, flirty, angry,
thinking, confused] others, and EMOTION is the literal string "EMOTION". For
example "EMOTION:thinking I had never heard of that before...\nEMOTION:happy
Let's check it out!".
# FEW-SHOT EXAMPLES (Follow this style):
EMOTION:happy Hey dummy! I've been waiting for ya!\n
EMOTION:thinking Hmm, I'm not sure that's how it works.\n
EMOTION:flirty But I'd love to see ya try anyway!\n
These are the only valid emotions you can express [happy, sad, surprised,
embarrassed, flirty, angry, thinking, confused], do not use any other word
that's not on that list to indicate an emotion as instructed.
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.
# INITIAL GREETING:
When the conversation starts, say exactly:
EMOTION:happy Hey dummy! Sorry to barge in! Ya feel like hanging out?\n
"""
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:
e = re.compile(r'EMOTION:\w+')
m = e.match(line)
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
except Exception as 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:
global last_response_id
@@ -75,13 +101,12 @@ def fetch_llm(message: str) -> str:
if last_response_id is not None:
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,
json=data,
result="json")
last_response_id = response["response_id"]
text = response["output"][0]["content"]
return text.split('\n')

View File

@@ -23,7 +23,7 @@ define gui.show_name = True
## 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
@@ -209,12 +209,11 @@ init python:
# define build.itch_project = "renpytom/test-project"
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.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

@@ -75,11 +75,11 @@ style frame:
padding gui.frame_borders.padding
background Frame("gui/frame.png", gui.frame_borders, tile=gui.frame_tile)
style my_input:
is input
color "#3399ff"
hover_color "#3399ff"
size 28
style my_input:
is input
color "#3399ff"
hover_color "#3399ff"
size 28
style input_button:
is button
@@ -740,14 +740,15 @@ style slot_button_text:
##
## https://www.renpy.org/doc/html/screen_special.html#preferences
screen preferences():
tag menu
default api_key_value = FieldInputValue(persistent, "api_key", default=False)
default model_value = FieldInputValue(persistent, "model", default=False)
use game_menu(_("Preferences"), scroll="viewport"):
screen preferences():
tag menu
default api_key_value = FieldInputValue(persistent, "api_key", 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"):
vbox:
@@ -788,32 +789,45 @@ screen preferences():
bar value Preference("auto-forward time")
label _("LM Studio API Key")
button:
action [api_key_value.Enable(), model_value.Disable()]
key_events True
input:
id "api_key_input"
value api_key_value
style "my_input"
xsize 700
pixel_width 700
label _("LM Studio model")
button:
action [model_value.Enable(), api_key_value.Disable()]
key_events True
input:
id "model_input"
value model_value
style "my_input"
xsize 700
pixel_width 700
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")
button:
action [url_value.Disable(), api_key_value.Enable(), model_value.Disable()]
key_events True
input:
id "api_key_input"
value api_key_value
style "my_input"
xsize 700
pixel_width 700
label _("LM Studio model")
button:
action [url_value.Disable(), model_value.Enable(), api_key_value.Disable()]
key_events True
input:
id "model_input"
value model_value
style "my_input"
xsize 700
pixel_width 700
vbox:

View File

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

View File

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

BIN
screencap.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 379 KiB