3 Commits

6 changed files with 95 additions and 49 deletions

View File

@@ -1,10 +1,12 @@
# Soul Droid Chat # Souldroid Chat
Chat with Anita, your favorite Soul Droid! 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 Soul Droids are © [Kieran Harris](https://www.deviantart.com/kieranharris), used without permission 😅 Anita and Souldroids are © [Kieran Harris](https://www.deviantart.com/kieranharris), used with love but without permission 😅
All art and sprites were generated with Gemini Nano Banana 2 and edited with GIMP and ImageMagick. All art and sprites were generated with Gemini Nano Banana 2 and edited with GIMP and ImageMagick.

View File

@@ -10,41 +10,34 @@ init python:
import re import re
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.
""" """
@@ -61,13 +54,18 @@ def parse_emotion(line):
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
try: try:
# Set basic request data. # Set basic request data.
headers = {"Authorization": f"Bearer {persistent.api_key}"} headers = {"Authorization": f"Bearer {persistent.api_key}"}
data = {"model": "gemma-3-4b-it", data = {"model": persistent.model,
"input": message, "input": message,
"system_prompt": SYSTEM_PROMPT} "system_prompt": SYSTEM_PROMPT}

View File

@@ -12,7 +12,7 @@
## ##
## The _() surrounding the string marks it as eligible for translation. ## The _() surrounding the string marks it as eligible for translation.
define config.name = _("Soul Droid Chat") define config.name = _("Souldroid Chat")
## Determines if the title given above is shown on the main menu screen. Set ## Determines if the title given above is shown on the main menu screen. Set
@@ -23,7 +23,7 @@ define gui.show_name = True
## The version of the game. ## The version of the game.
define config.version = "0.1" define config.version = "0.1.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
@@ -37,7 +37,7 @@ define gui.about = _p("""
## distribution. This must be ASCII-only, and must not contain spaces, colons, ## distribution. This must be ASCII-only, and must not contain spaces, colons,
## or semicolons. ## or semicolons.
define build.name = "SoulDroidChat" define build.name = "SouldroidChat"
## Sounds and music ############################################################ ## Sounds and music ############################################################
@@ -210,3 +210,11 @@ init python:
define config.minimum_presplash_time = 2.0 define config.minimum_presplash_time = 2.0
default persistent.api_key = '' 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,7 +75,17 @@ style frame:
padding gui.frame_borders.padding padding gui.frame_borders.padding
background Frame("gui/frame.png", gui.frame_borders, tile=gui.frame_tile) 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 input_button:
is button
yalign 1.0
key_events True
xysize (250, 25)
################################################################################ ################################################################################
## In-game screens ## In-game screens
@@ -730,11 +740,14 @@ style slot_button_text:
## ##
## https://www.renpy.org/doc/html/screen_special.html#preferences ## https://www.renpy.org/doc/html/screen_special.html#preferences
screen preferences(): screen preferences():
tag menu tag menu
use game_menu(_("Preferences"), scroll="viewport"): default api_key_value = FieldInputValue(persistent, "api_key", default=False)
default model_value = FieldInputValue(persistent, "model", default=False)
use game_menu(_("Preferences"), scroll="viewport"):
vbox: vbox:
@@ -775,9 +788,32 @@ screen preferences():
bar value Preference("auto-forward time") bar value Preference("auto-forward time")
label _("LM Studio API Key") label _("LM Studio API Key")
input value VariableInputValue("persistent.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
vbox: vbox:

View File

@@ -9,8 +9,9 @@ label start:
show anita happy with dissolve show anita happy with dissolve
$ response = fetch_llm('Start the conversation.')[0] $ response = fetch_llm('Start the conversation.')[0]
$ emotion, line = parse_emotion(response) $ sanitized = sanitize_speech(response)
show expression f'anita {emotion}' $ emotion, line = parse_emotion(sanitized)
show anita happy
a "[line]" a "[line]"
while True: while True:
@@ -20,9 +21,10 @@ label start:
while i < len(response): while i < len(response):
$ r = response[i].strip() $ r = response[i].strip()
$ s = sanitize_speech(r)
if r != '': if s != '':
$ emotion, line = parse_emotion(r) $ emotion, line = parse_emotion(s)
if emotion is not None: if emotion is not None:
show expression f'anita {emotion}' show expression f'anita {emotion}'
a "[line]" a "[line]"

BIN
screencap.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 379 KiB