Neural narratives in Python #11

I recommend you to check out the previous parts if you don’t know what this “neural narratives” thing is about. In short, I wrote in Python a system to have multi-character conversations with large language models (like Llama 3.1), in which the characters are isolated in terms of memories and bios, so no leakage to other participants like in Mantella. Here’s the GitHub repo.

Without further preamble…

I approached one of the students.

Let’s head to the library, then.

As I was having the big multi-char convo with Elara and two of her pals, as well as the other two people on my side, Elara treated her own companions as if she didn’t know them. Of course, she doesn’t actually know them: a character is only aware of that their bio says as well as the contents of their memories. Those people hadn’t been introduced in their memories yet. That’s an interesting problem to solve.

Of course, I could just make up their relationship by writing directly in their memories, but the whole point of having this app is for the large language model to act as a Dungeon Master. So, I’m going to create a whole new page on the site, one called Connections, that offers the opportunity to generate a relationship between two characters.

Here is the prompt I’ve put together so that the large language model will come up with a compelling, meaningful connection between two characters:

Instructions:

Using the character data provided for {name_a} and {name_b}, generate a meaningful and compelling connection between them. The connection should align with their personal information and memories. Ensure that the relationship is coherent, enriches their backstories, and is consistent with their individual characteristics. The connection does not need to be intimate or romantic; it can be any significant relationship such as friendship, rivalry, mentorship, familial ties, etc.

Character {name_a}:

{character_a_information}

Character {name_b}:

{character_b_information}

Your Task:

Using the above information, write a detailed description (approximately 3-5 sentences) of the connection between {name_a} and {name_b}. The description should:

Explain how they met or became connected.
Highlight the nature of their relationship (e.g., allies, rivals, mentor and protégé, siblings).
Incorporate elements from their personalities, backgrounds, likes, dislikes, secrets, or memories.
Be compelling and add depth to both characters.
Ensure that the connection is believable and enhances the narrative potential of their interactions.

Well, pulling off that Connections page was far quicker than I anticipated, but the app is already quite mature when it comes to examples of how stuff works.

The connection between them (that gets stored as a memory for both characters) isn’t shown to the user, to keep the intrigue. You can, of course, check it out in the JSON files that store the characters, but for storytelling purposes, it’s better to keep the secrecy. Like all other forms I’ve been dealing with, this one interacts with the server via AJAX, without actually reloading the page. Very slick.

As I was checking out how the ongoing dialogue was getting stored, I realized that at some point I had mistakenly stored the characters’ names instead of their descriptions, so I don’t think that the characters were aware of how the others looked as they spoke. This fix will open up plenty of dialogue flavor.

The error was worse than I anticipated. Turns out that when I refactored the gigantic manager class CharactersManager into five or so other classes, one of them a class named Character that handled everything related to character attributes, I had copy-pasted my way into horror: I was returning the property “name” even if what the user demanded was the description, the likes, the dislikes, the personality, etc. So these last couple of days, most of the prompts involving characters, if not all, have been missing about half of the proper data. That’s why you pytest the shit out of your app. Sorry, programming gods.

Anyway, long dialogue incoming:

That’s all for now. The following isn’t related to anything, but I must say it: recently I recommended the anime adaptation of Uzumaki. Not once, but twice. Well, consider that recommendation rescinded. Those motherfuckers really pulled a fast one on us. Dandadan is still cool, though.

Neural narratives in Python #10

I recommend you to check out the previous parts if you don’t know what this “neural narratives” thing is about. In short, I wrote in Python a system to have multi-character conversations with large language models (like Llama 3.1), in which the characters are isolated in terms of memories and bios, so no leakage to other participants like in Mantella. Here’s the GitHub repo.

As I considered my app this morning, a couple of things bothered me considerably: one, the CharactersManager class that handled pretty much any API requests related to characters had ballooned to insurmountable levels. It took me an hour and a half if not more to refactor it into four classes, given how entangled its code was with everything. Now I have a Character class that, provided with an identifier, handles the loading of its own related data, as well as saving it. That simplifies things a lot.

The second matter was far more troubling for me: every time I launched a request to the server that entailed a call to a large language model, the interface went unresponsive potentially for six seconds or more. That bothered me every time. The only real solution that I know is to use AJAX, a method of javascript programming that instead of actually sending a POST request to the server, it sort of simulates one through javascript, then returns the result as JSON data. That only has pros when it comes to the final execution, but reaching that point is troublesome. That’s what I’ve spent most day working on, and fortunately I’ve managed to achieve it for the most part: there are various parts of the app that now display “Processing…” on the button that generated the request, next to a spinner, and they return to normal once the request has been processed. If it entailed generating data that would appear on the page, then it gets displayed as well.

I’ve also changed some of the interface, particularly the buttons to subsections in Characters Hub and the Actions page. I think it looks quite cool.

As I was testing things while implementing AJAX into the chat page, I had the following idiotic conversation with the player character’s partner.

I have processed about 60 voice models of the 550 or so available in the RunPod server, so I’ll be able to enjoy many intriguing conversations through this narrative system (not to mention pretty exceptional smut).

Silliness aside, here are the descriptions of the player character and his posse as they head to the dreaded university.

I asked the AI to generate three more characters that would know or even be intimately connected with the arrogant student Elara Thorn.

That’s a perfectly normal group of people.

Last night, I thought about how this whole confrontation with Elara Thorn may play out. Were we going to accuse her of something and the LLM would play along and admit some wrongdoing? I thought about the notion of a character having a secret that even I wasn’t aware of. So I programmed just that: a section in Characters Hub that generates one or more secrets for any given character. Now all characters are created with some petty secrets of their own, which is bound to enrich their spoken parts, but this Secrets section is bound to generate some pretty hardcore secrets, as per the prompt.

You are to generate the secrets of a character. Use the provided information about the character, the world's conditions, and the specific locations involved.

Instructions:

To create secrets that are compelling and truly worth being hidden for the character, consider incorporating the following aspects:

Deep Personal Impact: The secret should have a profound effect on the character's life, shaping their personality, motivations, and actions. It might relate to a traumatic event, a pivotal choice, or a forbidden desire that they are desperate to keep concealed.
High Stakes and Consequences: The revelation of the secret should carry significant consequences for the character and potentially others. This could include personal ruin, endangerment of loved ones, loss of status, or catastrophic events within the story's world.
Moral Ambiguity: Secrets that involve morally gray areas make characters more complex and intriguing. The character might have done something considered unethical or made a compromise that weighs heavily on their conscience.
Inner Conflict and Guilt: The secret should create internal turmoil. Feelings of guilt, shame, or fear of discovery can drive the character’s behavior, making them more layered and realistic.
Hidden Identities or Double Lives: Characters may be living under an assumed identity or leading a double life. This creates tension as they balance their true self with the facade they present to the world.
Traumatic Past Events: A history involving trauma, such as witnessing or being involved in a catastrophic event, can be a secret that influences their current fears and motivations.
Forbidden Relationships: Involvement in relationships that are taboo or forbidden in their society adds emotional depth and high personal stakes if the secret is revealed.
Betrayal and Loyalty: A secret involving betrayal—whether the character betrayed someone or was betrayed—adds tension, especially if relationships with other characters are at risk.
Hidden Abilities or Powers: Concealing special abilities, especially in a world where such powers might be dangerous or outlawed, adds an element of suspense and fear of exposure.
Secret Motivations or Agendas: The character might have ulterior motives that conflict with their apparent goals or the goals of their allies, creating layers of intrigue.
Connection to Antagonists: A secret tie to the antagonist or antagonistic forces—such as being related to, indebted to, or blackmailed by them—complicates the character's role in the story.
Illegal or Illicit Activities: Participation in criminal activities, whether past or ongoing, provides clear reasons for secrecy and potential consequences if uncovered.
Prophecies or Destinies: Being the subject of a prophecy or destined for a significant role can be a burden the character tries to hide to avoid unwanted attention or responsibility.
Hidden Weaknesses or Vulnerabilities: Concealing physical, emotional, or psychological weaknesses to appear strong can add depth and tension, especially if these vulnerabilities are exploitable.
Knowledge of Critical Information: Possessing knowledge that others do not—such as impending disasters, secrets about other characters, or truths about the world's reality—can be dangerous to reveal.
Forbidden Knowledge or Research: Engaging in research or possessing knowledge that is forbidden or dangerous adds stakes, especially if discovery could lead to severe punishment.
Family Secrets and Lineage: Hidden heritage, such as being the descendant of a notable or infamous figure, can shape the character's identity and the perceptions of others.
Past Failures or Mistakes: A significant failure or mistake in the character's past that haunts them, influencing their present actions and decisions.
Survivor's Guilt: Being the sole survivor of an event and feeling responsible can be a heavy burden that the character keeps to themselves.
Secret Alliances or Memberships: Belonging to a secret society, cult, or group can add complexity, especially if their goals conflict with those of others.
Hidden Assets or Treasures: Possessing or knowing the location of valuable items can make the character a target and provide motivation to keep it hidden.
Internal Struggles with Identity: Questions about one's own identity, such as gender identity, sexual orientation, or personal beliefs that are not accepted in their society.
Mental Health Issues: Concealing mental health struggles due to fear of stigma or repercussions can add realism and depth to the character.
Unfulfilled Vengeance: Harboring a secret desire for revenge that drives the character's actions without others realizing their true intent.
Divine or Supernatural Experiences: Having had an encounter with the divine or supernatural that is disbelieved or ridiculed by society, leading them to keep it secret.

To ensure these secrets are compelling and worth hiding:

Integrate with Character Development: The secret should be a key part of the character's backstory and influence their development throughout the story.
Create Tension and Suspense: The possibility of the secret being discovered should create ongoing tension, affecting interactions and decisions.
Impact Relationships: The secret should have the potential to alter relationships with other characters significantly if revealed.
Drive the Plot Forward: The secret can serve as a catalyst for events in the story, creating twists, conflicts, or revelations that keep the narrative engaging.
Provide a Strong Motivation for Secrecy: The character should have clear, understandable reasons for keeping the secret hidden, such as fear of harm, shame, or protecting others.
Offer Opportunities for Revelation: Build moments into the story where the secret might be revealed, forcing the character to make tough choices.
Reflect Universal Themes: Themes like redemption, identity, betrayal, or the nature of truth can make the secret more relatable and impactful.

Provided Information:

{places_descriptions}

Character Information:
Name: {name}
Description: {description}
Personality: {personality}
Profile: {profile}
Likes: {likes}
Dislikes: {dislikes}
Speech Patterns: {speech_patterns}
Health: {health}
Equipment: {equipment}
Memories:
{memories}

Existing Secrets: {secrets}

Example Format:

"has developed romantic feelings for her brother, feels guilty about accidentally causing her cat's death, ..."

Your Task:

Using the above instructions and information, craft compelling, meaningful secrets for the character, adding to their existing secrets.

Anyway, that’s all I have time for today. See ya.

Neural narratives in Python #9

I recommend you to check out the previous parts if you don’t know what this “neural narratives” thing is about. In short, I wrote in Python a system to have multi-character conversations with large language models (like Llama 3.1), in which the characters are isolated in terms of memories and bios, so no leakage to other participants like in Mantella. Here’s the GitHub repo.

The previous part ended with our hardened player character returning home after an encounter with an arrogant student. At three in the morning, the following happens:

Now, a fundamental problems happens. How do you find clues in a room without making it up through a conversation? You don’t. A Dungeon Master of sorts would decide on the results of the investigation. That means I need to program a whole new thing in.

Maybe a week ago, I programmed the concept of a Goal Resolution: you gave the large language model a goal and it decided whether or not it was successful, and to what degree, according to the information provided and its own whims. But I quickly realized that it was too broad a concept: when you’re trying to accomplish something, you’re not pursuing “a goal”; you’re trying to investigate, to research, to trade, etc. A few days ago I created the concept of a Research action, which is constrained solely to that notion. Here is, why not, the prompt I send to the AI so it will come up with a resolution:

You are to generate a detailed narrative describing the player's attempt at a Research action within a rich, immersive world. Use the provided information about the player, their followers, the world's conditions, and the specific locations involved. Your narrative should be engaging and realistic, reflecting the player's abilities and circumstances.

Instructions:

1. Viability and Realism Assessment:

- Carefully consider whether the player's research goal is achievable based on:
The information about the player.
The abilities and resources of any followers accompanying the player.
The characteristics of the location, including available resources, accessibility, and any restrictions.
The time of day and how it might affect the research attempt.

2. Narrative of the Research Attempt:

Write two or three descriptive paragraphs detailing the player's research endeavor.
Include specific actions taken by the player and their followers.
Describe interactions with the environment, use of equipment, and any obstacles or challenges faced.
Ensure the attempt aligns with the world's lore and the player's personal history.

3. Outcome - Impact of the Research:

Determine the results of the research action based on the assessment.
You must be specific about what the player has discovered: this is the memory that will get saved.
Detail any new knowledge gained, abilities unlocked, or crucial information discovered.
If the research was partially successful or failed, explain what was learned or why it didn't succeed fully.

4. Consequences - Cost of the Research:

- Describe the costs associated with the research action, such as:
Time consumed and its implications.
Use or loss of resources and equipment.
Physical or mental strain on the player and followers.
Potential negative repercussions, such as drawing unwanted attention or causing disturbances.
Use your judgement to determine the extent of the consequences, according to all the information provided.
Important: the consequences are an account of the costs of the research effort, looking back now that it's finished. Do not continue the narrative nor project into the future.

5. Tone and Style:

Write in the third person, past tense, maintaining an engaging and immersive narrative style.
Ensure consistency with the established world setting and the player's character.

Provided Information:

- Time of Day: {hour} {time_of_day}
- World Description: {world_description}
- Region Description: {region_description}
- Area Description: {area_description}
{location_description}
Player and Followers Information:
- Name: {player_name}
- Description: {player_description}
- Personality: {player_personality}
- Profile: {player_profile}
- Likes: {player_likes}
- Dislikes: {player_dislikes}
- Speech patterns: {player_speech_patterns}
- Health: {player_health}
- Equipment: {player_equipment}
-----
{followers_information}
- Memories:
{combined_memories}

- Research Goal: {research_goal}

Example Format:

"Seeking to unravel the mysteries of the ancient sigils, the tall, cloaked figure of [Player Name] entered the dimly lit archives of the Grand Library. Clutching the worn tome of cryptic symbols, they, alongside their ever-curious companion [Follower Name], began sifting through scrolls and manuscripts..."

Your Task:

Using the above instructions and information, craft a compelling narrative that captures the essence of the player's Research action, reflecting both the potential rewards and the inherent costs.

That worked well enough, but investigating a missing teenager’s room isn’t “research,” so I need to create a whole new page for my app.

Creating the Investigate page took me a few hours. In the end, the user has to provide an investigation goal and the facts already known about the case (I don’t trust the AI enough to be able to glean them from the memories of the involved characters). It produced the following outcome and consequences:

Oho, the plot thickens! It seems I’ll make a visit to the university in the morning to interrogate this college student. First, though, I needed to know the opinion of the missing girl’s father.

We hurried to the police station to meet my character’s partner.

I didn’t plan to, but this is a fantastic opportunity to test the Research action. Yet another example of how dynamic and unpredictable this system is.

That worked out perfectly. So this Elara Thorn and her team may have bitten more than they could chew, and maybe made some innocents disappear in the process.

Through testing both the Investigate and the Research actions, I’ve learned that the Consequences part that I ask the LLM to produce is completely useless. Great. That saves some computing power.

My player character’s partner returned to the station.

Neural narratives in Python #8

I recommend you to check out the previous parts if you don’t know what this “neural narratives” thing is about. In short, I wrote in Python a system to have multi-character conversations with large language models (like Llama 3.1), in which the characters are isolated in terms of memories and bios, so no leakage to other participants like in Mantella. Here’s the GitHub repo.

The last entry ended when I had managed to add a hole-in-the-wall to the ongoing story. Let’s enter it and allow the player character to describe it.

Every time you enter a new location, the app checks if there is a list of character generation guidelines already created for this combination of places, and if there isn’t, it generates that list. After a short while, the app produced the following:

That one about a brilliant university student sounds like a great contrast to the existing characters. I pressed the button that generates a new character, and at the end of that process, the app presented me with the following portrait:

That looks real good. Great job on the fingers, AI. However, the large language model named this character Elara Thorn, the exact name that was generated for a character in another one of my trial-run stories with this system. Perhaps at some point I’ll need to program a way of reading names from a gigantic list, then presenting the AI with about twenty random ones to choose from.

Anyway, let’s have a multi-char convo.

After such a tense conversation, I figured that the player character would reflect on it, as well as his general circumstances. I happened to have implemented a system for self-reflection: the large language model looks at the character’s memories, then writes a sort of journal entry from the first-person perspective. It gets saved along with the rest of the memories. That helps color the dialogue and in general make the character sound more intelligent. Of course, now I generate audio files of those self-reflections as well.

My hardened player character returned home. I’ll let him describe his living arrangements.

I can imagine the player character returning to work the following day, only to be introduced to some troubled citizen who will present him with a case. However, I have also programmed a way of generating story concepts, interesting situations, interesting dilemmas, and interesting goals, in case the user isn’t too sure how to continue. Let’s generate a few.

Here are a few intriguing concepts the app has generated. When crafting any of these notions, the large language model is presented with the player’s information and that of his followers, and also all the available information about his location (world, region, area, and possibly location).

I should probably turn those post-its into something else, like papers or something, because such long texts look funky in a vertical format. That’s a minor issue in any case.

Investigating a series of gruesome murders connected to the otherworldly horrors sounds good for a story, and even more if the encounter with the arrogant student earlier does hint at a larger plot involving a secret society of reckless scholars. I also like the notion of a neighbor calling on his door to ask for his help because the person’s daughter has gone missing. Also, the sort of post-apocalyptic story of the grizzled detective leading a ragtag group as they fight against the pouring eldritch horrors sounds pretty fucking dope.

How about general goals?

Some of those are interesting. The disappeared scholar could easily be the aforementioned college girl, so how would the detectives feel about investigating her disappearance? The goal about infiltrating a cult makes me think that I would need a way of altering a character’s description so they can pass undercover, because other characters are fed the participants’ description during a conversation. And again, someone is requesting the detective’s help to find their missing child.

Neural narratives in Python #7

I recommend you to check out the previous parts if you don’t know what this “neural narratives” thing is about. In short, I wrote in Python a system to have multi-character conversations with large language models (like Llama 3.1), in which the characters are isolated in terms of memories and bios, so no leakage to other participants like in Mantella. Here’s the GitHub repo.

Last time, I managed to nail creating voice lines for the dialogues of my app, relying on a RunPod server dedicated to generating audio. Now I want to go on a serious test run of app to see what it lacks.

Starting from scratch, I created a new world, a new region of that world, a new area of that region, and a new location of that area. I won’t detail the specifics, because the app itself should do it like a story would do, so if in the course of testing the system I feel that something must be implemented to cover for the shortcomings, I will do so.

The process of creating a new playthrough requires you to provide some notion of the character you want to play as. I ended up with a good depiction of the guy.

I have the initial setting and the player character (including his automatically assigned voice model). A story needs other characters, so I went to the section that shows the character creation guidelines that have been generated for this combination of places.

I turned them into post-its. Anyway, my protagonist is a police detective, so he could do with a partner. I grabbed the second guideline and made her a woman.

The app doesn’t allow you to access most of the specific data of a character except in very paricular circumstances, so in general, you have to glean the specifics of a character from their looks and the conversations you have with them.

In a story, you need some sense of where you are. There’s a system in place for the LLM to generate a description from the first-person perspective of the player character, and at this point it’s trivial to generate a voice line for it:

Let’s interact with the sole other character around (for now). As I was running a conversation with the protagonist’s partner, I ran across the first issue: when the app had to generate a voice line for a bit of ambient text, the server (the RunPod pod) returned a 404. I guess that even if a pod is technically running, it could intermittently produce 404 errors for whatever reason. I guess I’ll need to program in some retry system to cover these cases.

I did do that. Let’s continue.

The “stop your a coffee” was my blunder. Old, stupid fingers.

The couple of grizzled detectives exited the police station, back to the grim city surrounding it.

Now I want my characters to move to the mentioned location, some bar. Even though I didn’t create any other locations for this run other than the police station, there is a lingering issue with the interface: when plenty of possible locations exist, if you press the button “Search for location,” it may link locations you don’t want (like a cave, a hospital, etc.). Now that I was looking specifically for a bar, I figured that I may as well fix this issue.

It took quite a while, but now the user can only search locations by a type. In fact, if no locations are available, because they have already been used or they don’t match the area’s categories (you don’t want a fantasy bar in a cosmic horror story), the select and the button will be disabled.

Well, that was all for today. I expected to do more, but reworking that interface was arduous.

Neural narratives in Python #6

I recommend you to check out the previous parts if you don’t know what this “neural narratives” thing is about. In short, I wrote in Python a system to have multi-character conversations with large language models (like Llama 3.1), in which the characters are isolated in terms of memories and bios, so no leakage to other participants like in Mantella. Here’s the GitHub repo.

In the previous entry, I came up with the notion of producing audio voice lines for the conversations. Mantella had spoiled me in that regard: hearing those fictional characters answering you in reasonably good voices while you stared at them did wonders for immersion. And it was a bad idea to shove that possibility into my mind, because it prevented me from sleeping last night. Instead, I moved to my desk at three in the morning and started implementing it. Now, every generated character gets assigned a voice model according to their peculiarities, and each speech turn produces voice lines that the user can play through clicking the speech bubbles. It works perfectly.

I learned that it’s a terrible idea to play audio server-side, because it crashes the server. Flask, the web framework that my app is programmed in, or maybe it happens in all web systems, also doesn’t allow the client to access any file in the server, so I had to move all the audio-playing logic to Javascript.

Given this example chat I had with a new character, who had been assigned a matching voice automatically among the relatively few I’ve introduced into the system so far:

The short convo produced this audio exchange:

Like in the original Mantella system, the quality of voice models varies greatly; sometimes they sound like theater students reading a script, recorded on a home mic. Also, the process of generating the clips sometimes shears the very end of their final sentence. Still, I can hardly complain. Listening to the characters adds so much life to the conversations you can have through this app that I see myself enjoying it for a long time to come (and not only for smut).

I’m amazed that I got this running. So, how did it happen?

In the beginning, I thought that setting up my own, local XTTS server (XTTS being a model for generating voice lines) was a good idea. I struggled through every step of the way for a few hours, fighting against obscure documentation, until I finally managed to generate a sample voice line, only to find out that it sounded like ass. Why, I have no idea. So I discarded that notion and instead I looked into Mantella’s codebase, which is up at GitHub, to see how they connected to the RunPod pods to request voice generation. RunPod is a sort of online renting system of computer and server time: you can set up a pre-configured little server that all it does is generate voice lines, and as long as you can connect to it, you’re set. Only costs seventeen cents an hour, too. Once I managed to query the list of available voice models from the RunPod pod, I knew I was going to get through this thing.

So, I had a list of all possible voice models I could rely on, and it turned out to be about five hundred fifty. They are trained from game voices, so there’s a whole breadth of possible voices one can use. How to classify them? Should I create a page on my site with a simple select box, letting the user (meaning me) scroll through a list that long?

ChatGPT, even its latest Orion preview version, clarified that it knows of no online service that could classify the more than five hundred voice samples I had produced from those voice models. I would have to do it manually, but in the beginning it would be enough with having introduced twenty or so models into the system. What tags can be applied to a voice? I relied on ChatGPT to figure that out. Now that I have that list, classifying each voice model is as easy, but time consuming, as listening to that sample on a loop while adding appropriate tags. I have ended up, so far, with the following JSON file of voice models:

{
  "npcmmel": [
    "MALE",
    "ADULT",
    "CONFIDENT",
    "STEADY",
    "SMOOTH",
    "CLEAR",
    "FORMAL",
    "CHARMING",
    "NO SPECIAL EFFECTS"
  ],
  "npcmlucasmiller": [
    "MALE",
    "ADULT",
    "CALM",
    "FAST-PACED",
    "SMOOTH",
    "CASUAL",
    "KIND",
    "NO SPECIAL EFFECTS"
  ],
  "robotmsnanny": [
    "FEMALE",
    "YOUNG ADULT",
    "STEADY",
    "WARM",
    "CASUAL",
    "MELODIC",
    "YOUTHFUL",
    "NO SPECIAL EFFECTS"
  ],
  "npcma951": [
    "MALE",
    "ADULT",
    "ANXIOUS",
    "SLOW",
    "AIRY",
    "SKEPTICAL",
    "NO SPECIAL EFFECTS"
  ],
  "npcfphyllisdaily": [
    "FEMALE",
    "ADULT",
    "STOIC",
    "SLOW",
    "MONOTONE",
    "INSTRUCTIONAL",
    "CALCULATING",
    "NO SPECIAL EFFECTS"
  ],
  "femalechild": [
    "FEMALE",
    "CHILDLIKE",
    "PLAYFUL",
    "STEADY",
    "AIRY",
    "MELODIC",
    "INNOCENT",
    "NO SPECIAL EFFECTS"
  ],
  "femaleyoungeager": [
    "FEMALE",
    "YOUNG ADULT",
    "HOPEFUL",
    "FAST-PACED",
    "CLEAR",
    "INTENSE",
    "OPTIMISTIC",
    "NO SPECIAL EFFECTS"
  ],
  "femalevampire": [
    "FEMALE",
    "MIDDLE-AGED",
    "ARROGANT",
    "STEADY",
    "SMOOTH",
    "AUTHORITATIVE",
    "CYNICAL",
    "NO SPECIAL EFFECTS"
  ],
  "femalekhajiit": [
    "FEMALE",
    "ADULT",
    "CALM",
    "STEADY",
    "GRAVELLY",
    "CASUAL",
    "PHILOSOPHICAL",
    "NO SPECIAL EFFECTS"
  ],
  "femaleuniqueghost": [
    "FEMALE",
    "YOUNG ADULT",
    "RESIGNED",
    "STEADY",
    "ETHEREAL",
    "MELODIC",
    "INNOCENT",
    "GHOSTLY"
  ],
  "femaleghoul": [
    "FEMALE",
    "ADULT",
    "MENACING",
    "STEADY",
    "RASPY",
    "INTENSE",
    "ENERGETIC",
    "NO SPECIAL EFFECTS"
  ],
  "femaleboston": [
    "FEMALE",
    "ADULT",
    "CALM",
    "DRAWLING",
    "SOFT-SPOKEN",
    "WARM",
    "FLIRTATIOUS",
    "SULTRY",
    "NO SPECIAL EFFECTS"
  ]
}

I wrote a function that narrows down the list of possible categories of tags: gender, age, emotion, tempo, volume, texture, style, personality, and special effects. If at some point there’s no matching voice models, it returns a random one from the previous filtering. I’ll probably program in the characters section of the site a simple button that redoes the process for any existing character, in case any other random fitting voice may work better.

That’s all, I guess. When I first got the idea about programming this conversation system with characters controlled by large language models, I knew that programming the multi-char convos would be the most difficult thing. The second most difficult thing that I pictured was actually making them talk out loud. No idea what big thing could be coming next. Anyway, back to the brothel.

EDIT: here’s a multi-char convo in audiobook form.