Compare commits

...

No commits in common. "experimental" and "main" have entirely different histories.

66 changed files with 98 additions and 842 deletions

View file

@ -214,5 +214,3 @@ __marimo__/
# Streamlit # Streamlit
.streamlit/secrets.toml .streamlit/secrets.toml
# custom
voicesline/playersname

5
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,5 @@
{
"cSpell.words": [
"potionchoice"
]
}

View file

@ -1,7 +1,7 @@
import logging import logging
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
log.setLevel(logging.INFO) log.setLevel(logging.DEBUG)
formatter = logging.Formatter( formatter = logging.Formatter(
"%(asctime)s \033[1m%(levelname)s\033[0m\033[0m %(message)s" "%(asctime)s \033[1m%(levelname)s\033[0m\033[0m %(message)s"
) )

View file

@ -3,19 +3,11 @@ from typing import Dict, List, Optional
from dataclasses import dataclass from dataclasses import dataclass
import sys import sys
from logger import log from logger import log
from finger_count import * from fuzzywuzzy import process
from playsound import playsound
import wave
from piper import PiperVoice
import os
from art import *
from translation import *
@dataclass @dataclass
class Player: class Player:
name: str name: str
alias: str
role: str role: str
alive: bool = True alive: bool = True
protected: bool = False protected: bool = False
@ -34,8 +26,6 @@ class Game:
# Used potions # Used potions
self.used_potions: [list[str]] = [] self.used_potions: [list[str]] = []
# Language
self.language: str = "fr"
# ------------------------- # -------------------------
# setup / I/O # setup / I/O
# ------------------------- # -------------------------
@ -46,7 +36,7 @@ class Game:
""" """
players_file = "players.txt" players_file = "players.txt"
roles_file = "roles.txt" roles_file = "roles.txt"
words_file = "randoms_words.txt"
log.info("Loading lists...") log.info("Loading lists...")
try: try:
@ -63,29 +53,20 @@ class Game:
log.critical("Roles file not found: %s", roles_file) log.critical("Roles file not found: %s", roles_file)
return None return None
try:
with open(words_file, "r", encoding="utf-8") as rf:
alias_list = [line.strip() for line in rf if line.strip()]
except FileNotFoundError:
log.critical("Roles file not found: %s", words_file)
return None
log.info("Load complete!") log.info("Load complete!")
return {"players": player_names, "roles": roles_list, "alias": alias_list} return {"players": player_names, "roles": roles_list}
# ------------------------- # -------------------------
# player / role assignment # player / role assignment
# ------------------------- # -------------------------
def give_roles_to_players(self, player_names: Optional[List[str]] = None, def give_roles_to_players(self, player_names: Optional[List[str]] = None,
roles_list: Optional[List[str]] = None, roles_list: Optional[List[str]] = None) -> None:
alias_list: Optional[List[str]] = None) -> None:
""" """
Assign a random role to each player. Accepts optional lists (returned by load_lists). Assign a random role to each player. Accepts optional lists (returned by load_lists).
If lists are not supplied, it will attempt to read files itself. If lists are not supplied, it will attempt to read files itself.
""" """
number = 0
if not player_names: if not player_names:
log.info("Players are not initialized") log.info("Players are not initialized")
return return
@ -96,59 +77,16 @@ class Game:
log.error("Not enough roles for players (roles: %d, players: %d)", log.error("Not enough roles for players (roles: %d, players: %d)",
len(roles_list), len(player_names)) len(roles_list), len(player_names))
return return
if len(alias_list) < len(player_names):
log.error("Not enough randoms words for players (words: %d, players: %d)",
len(alias_list), len(player_names))
return
available_roles = list(roles_list) available_roles = list(roles_list)
random.shuffle(available_roles) random.shuffle(available_roles)
available_alias = list(alias_list)
random.shuffle(alias_list)
# clear any existing players (safe to reassign) # clear any existing players (safe to reassign)
self.players.clear() self.players.clear()
# clear any player name audio
if not os.path.exists("./voicesline/playersname"):
os.makedirs("./voicesline/playersname")
for filename in os.listdir("./voicesline/playersname"):
file_path = os.path.join("./voicesline/playersname", filename)
# Check if it is a file (not a subdirectory)
if os.path.isfile(file_path):
os.remove(file_path) # Remove the file
for filename in os.listdir("./voicesline/alias"):
file_path = os.path.join("./voicesline/alias", filename)
# Check if it is a file (not a subdirectory)
if os.path.isfile(file_path):
os.remove(file_path) # Remove the file
if self.language == "fr":
voice = PiperVoice.load("./voices/fr/fr_FR-tom-medium.onnx")
elif self.language == "en":
voice = PiperVoice.load("./voices/en/en_GB-alan-medium.onnx")
else:
log.info("Unknow language")
for name in player_names: for name in player_names:
chosen_role = available_roles.pop() chosen_role = available_roles.pop()
chosen_alias = available_alias.pop() self.players[name] = Player(name=name, role=chosen_role)
with wave.open(f"./voicesline/playersname/{name}.wav", "wb") as wav_file:
voice.synthesize_wav(name, wav_file)
with wave.open(f"./voicesline/alias/{chosen_alias}.wav", "wb") as wav_file:
voice.synthesize_wav(chosen_alias, wav_file)
number += 1
self.players[number] = Player(name=name, role=chosen_role, alias=chosen_alias)
self.play_sound("players", name.capitalize())
self.play_sound("prompts", "first_wakes")
if name == "Dan":
self.play_sound("prompts", "cheater")
self.show_text(chosen_role)
self.show_text(chosen_alias)
time.sleep(3)
os.system('clear')
self.play_sound("players", name.capitalize())
self.play_sound("prompts", "sleep_again")
log.debug("Assigned roles:") log.debug("Assigned roles:")
for player in self.players.values(): for player in self.players.values():
@ -157,90 +95,56 @@ class Game:
# ------------------------- # -------------------------
# utilities # utilities
# ------------------------- # -------------------------
def clear_data(self) -> None:
for player in self.players:
self.players[player].protected = False
def couple(self) -> None:
os.system('clear')
self.play_sound("prompts", "lovers")
log.debug(self.lovers)
for player in self.lovers:
log.info(self.players[player].alias)
self.play_sound("alias", self.players[player].alias)
self.show_text(self.players[player].role)
time.sleep(10)
def show_text(self, text: str) -> None:
if self.language == "fr":
if text in FR:
Art=text2art(FR.get(text))
print(Art)
else:
Art=text2art(text)
print(Art)
if self.language == "en":
Art=text2art(text)
print(Art)
def get_remaining_roles(self) -> List[str]:
remaining_roles = []
for player in self.players:
if self.players[player].alive:
remaining_roles.append(self.players[player].role)
log.DEBUG(remaining_roles)
return remaining_roles
def number_2_name(self, player_number: int):
player_name = self.players[player_number].name
return player_name
def play_sound(self, type, soundinfo) -> None:
if type == "prompts":
f = f"./voicesline/prompts/{self.language}/{soundinfo}.wav"
elif type == "roles":
f = f"./voicesline/roles/{self.language}/{soundinfo}.wav"
elif type == "players":
f = f"./voicesline/playersname/{soundinfo}.wav"
elif type == "alias":
f = f"./voicesline/alias/{soundinfo}.wav"
else:
log.debug("An error occurred")
return
time.sleep(0.2)
playsound(f)
def select_someone(self, prompt: Optional[str] = None) -> Optional[str]: def select_someone(self, prompt: Optional[str] = None) -> Optional[str]:
""" """
Prompt the user to enter a player name until a valid one is entered. Prompt the user to enter a player name until a valid one is entered.
Returns None on EOF/KeyboardInterrupt. Returns None on EOF/KeyboardInterrupt.
""" """
prompt = prompt or "show_number" prompt = prompt or "Enter the name of the player: "
#self.play_sound("prompts", prompt)
while True: while True:
selected = count_fingers() selected = input(prompt).strip()
# exact match # exact match
if selected in self.players: if selected in self.players:
return selected return selected
# fuzzy matching
match = process.extract(selected, self.players, limit=1)
log.debug(match)
if match:
fuzz_player, fuzz_score = match[0][0], match[0][1] # player name, score
log.debug(fuzz_player)
log.debug(fuzz_score)
if fuzz_score >= 60:
log.info("You meant %s!", fuzz_player.name)
log.debug("Fuzz score: %s" % fuzz_score)
return fuzz_player.name
def choose_between(self, options): def choose_between(self, options):
#Prompt the user to choose between options until a valid one is entered #Prompt the user to choose between options until a valid one is entered
numbered_options = " ".join([f"{i+1}: {option}" for i, option in enumerate(options)])
log.info(f"Choose between {numbered_options}:") options_available = ', '.join(map(str, options))
#Make a prompt with a list of options, accepting either a list or a tuple.
prompt = f"Choose between {options_available}: "
while True: while True:
selected = count_fingers() selected = input(prompt)
try:
index = int(selected) - 1 if selected in options:
if 0 <= index < len(options): return selected
chosen_option = options[index]
log.debug("You chose %s!", chosen_option) # fuzzy matching
return chosen_option match = process.extract(selected, options, limit=1)
else: log.debug(match)
log.warning("Invalid number entered.") if match:
except ValueError: fuzz_option, fuzz_score = match[0][0], match[0][1] #options, score
log.warning("Invalid input. Please enter an option or number.") log.debug(fuzz_option)
log.debug(fuzz_score)
if fuzz_score >= 60:
log.info("You meant %s!", fuzz_option)
log.debug("Fuzz score: %s" % fuzz_score)
return fuzz_option
# ------------------------- # -------------------------
# game actions # game actions
# ------------------------- # -------------------------
@ -252,7 +156,7 @@ class Game:
self.lovers = (player1, player2) self.lovers = (player1, player2)
log.debug("Players now put in love: %s <-> %s", self.players[player1], self.players[player2]) log.debug("Players now put in love: %s <-> %s", player1, player2)
log.info("They are now bounded by love.") log.info("They are now bounded by love.")
log.info("They wake up and reveal their role to each other.") log.info("They wake up and reveal their role to each other.")
@ -316,22 +220,21 @@ class Game:
log.debug("%s -> role: %s, alive: %s, Protected: %s", log.debug("%s -> role: %s, alive: %s, Protected: %s",
p.name, p.role, p.alive, p.protected) p.name, p.role, p.alive, p.protected)
# lovers # lovers
log.debug(f"Lovers: {self.lovers}") log.debug("Lovers: %s" % ",".join(self.lovers))
def role(func): def role(func):
"""Decorator for roles""" """Decorator for roles"""
def wrapper(self, *args, **kwargs): def wrapper(*args, **kwargs):
# the role is the name of the function thats decorated # the role is the name of the function thats decorated
role = func.__name__.capitalize() role = func.__name__.capitalize()
os.system('clear') log.info("%s wakes up." % role)
if role in self.get_remaining_roles(): func(*args, **kwargs)
self.play_sound("roles", role) log.info("%s goes back to sleep." % role)
self.play_sound("prompts", "wakes")
func(self, *args, **kwargs) # workaround to call status from the instance
self.play_sound("roles", role) # is it ok? no idea but i dont have self anyway
self.play_sound("prompts", "sleep_again") instance = args[0]
log.INFO("%s goes back to sleep." % role) instance.status()
self.status()
return wrapper return wrapper
@ -342,12 +245,12 @@ class Game:
@role @role
def cupidon(self) -> None: def cupidon(self) -> None:
"""Interactively pick two players to link by love.""" """Interactively pick two players to link by love."""
self.play_sound("prompts", "linked_death") log.info("Choose two people to be linked to death.")
self.play_sound("prompts", "first_bound") log.info("Give me the first person that shall be bounded!")
p1 = self.select_someone() p1 = self.select_someone()
if p1 is None: if p1 is None:
return return
self.play_sound("prompts", "second_bound") log.info("What about the second one?")
p2 = self.select_someone() p2 = self.select_someone()
if p2 is None: if p2 is None:
return return
@ -359,23 +262,21 @@ class Game:
@role @role
def savior(self) -> None: def savior(self) -> None:
"""Interactively choose someone to protect.""" """Interactively choose someone to protect."""
self.play_sound("prompts", "choose_protect") log.info("Choose someone to protect.")
protected = self.select_someone() protected = self.select_someone()
if protected is None: if protected is None:
return return
self.players[protected].protected = True self.players[protected].protected = True
log.debug("Protected: %s", protected) log.debug("Protected: %s", protected)
@role @role
def witch(self) -> None: def witch(self) -> None:
"""Interactively choose to kill or revive someone""" """Interactively choose to kill or revive someone"""
self.play_sound("prompts", "potions_choice") log.info("With the Revive Potion, you can revive someone, and with the Death Potion, you can kill someone. You can only use each potion once, and you can also choose to do nothing.")
self.play_sound("prompts", "might_die") log.info("Players who might die are %s", ' '.join(map(str, self.dead_this_night)))
for player in self.dead_this_night:
self.show_text(self.players[player].name)
if len(self.used_potions) >= 2: if len(self.used_potions) >= 2:
self.show_text("Vous avez utilisé toutes vos potions. ") log.info("You already used all of your potions. ")
time.sleep(5)
else: else:
while True: while True:
options = ("Death", "Revive", "Nothing") options = ("Death", "Revive", "Nothing")
@ -387,9 +288,9 @@ class Game:
self.revive(player) self.revive(player)
return return
elif player not in self.dead_this_night and not self.players[player].alive: elif player not in self.dead_this_night and not self.players[player].alive:
self.show_text("Tu ne peux pas faire revivre cette personne.") log.info("You cannot bring this person back to life because they have been buried.")
elif self.players[player].alive: elif self.players[player].alive:
self.show_text("Cette personne n'est pas morte.") log.info("This player is not dead.")
else: else:
log.info("Unknown error: Invalid player choice") log.info("Unknown error: Invalid player choice")
return return
@ -398,15 +299,14 @@ class Game:
if self.players[player].alive: if self.players[player].alive:
self.used_potions.append("Death") self.used_potions.append("Death")
self.kill(player) self.kill(player)
time.sleep(1)
return return
elif not self.players[player].alive: elif not self.players[player].alive:
self.show_text("Ce joueur est déjà mort.") log.info("This player is already dead.")
else: else:
log.info("Unknown error: Invalid player choice") log.info("Unknown error: Invalid player choice")
return return
elif potionchoice == "Nothing": elif potionchoice == "Nothing":
self.show_text("Vous ne faites rien cette nuit.") log.info("You are not doing anything tonight.")
return return
elif potionchoice in self.used_potions: elif potionchoice in self.used_potions:
log.info("You already used this potion.") log.info("You already used this potion.")
@ -415,7 +315,7 @@ class Game:
return return
@role @role
def werewolf(self) -> None: def werewolf(self) -> None:
self.play_sound("prompts", "werewolf_choice") log.info("You will choose someone to kill.")
player = self.select_someone() player = self.select_someone()
self.kill(player) self.kill(player)
@ -425,56 +325,21 @@ class Game:
while True: while True:
player = self.select_someone() player = self.select_someone()
if "Seer" == self.players[player].role: if "Seer" == self.players[player].role:
self.show_text("You can't see your own role.") log.info("You can't see your own role.")
else: else:
self.show_text(self.players[player].role) log.info(f"{player} is a {self.players[player].role}")
time.sleep(3)
return return
@role
def hunter(self) -> None:
self.play_sound("prompts", "hunter")
player = self.select_someone()
self.kill(player)
# ------------------------- # -------------------------
# game flow # game flow
# ------------------------- # -------------------------
def first_night_cycle(self) -> None: def first_day_cycle(self) -> None:
self.play_sound("prompts", "fall_asleep_all") log.info("All the villagers fall asleep.")
self.cupidon() self.cupidon()
self.couple()
self.savior() self.savior()
self.werewolf() self.werewolf()
self.witch() self.witch()
self.seer() self.seer()
self.day()
def night(self) -> None:
self.dead_this_night = []
self.play_sound("prompts", "fall_asleep_all")
self.savior()
self.werewolf()
self.witch()
self.seer()
self.day()
def day(self) -> None:
self.play_sound("prompts", "wake_up_all")
self.play_sound("prompts", "night_deaths")
for player in self.dead_this_night:
self.play_sound("players", self.number_2_name(player))
for player in self.dead_this_night:
if self.players[player].role == "Hunter":
self.hunter()
self.play_sound("prompts", "village_vote")
while True:
if count_fingers(stable_duration=4) == 6:
break
self.play_sound("prompts", "now_show_number")
player = self.select_someone()
self.kill(player)
self.night()
@ -489,8 +354,8 @@ if __name__ == "__main__":
try: try:
if loaded: if loaded:
# start the game! # start the game!
game.give_roles_to_players(loaded["players"], loaded["roles"], loaded["alias"]) game.give_roles_to_players(loaded["players"], loaded["roles"])
game.first_night_cycle() game.first_day_cycle()
# CTRL+C # CTRL+C
except KeyboardInterrupt: except KeyboardInterrupt:

View file

@ -1,6 +1,10 @@
Anavoi Marie
Jean
Paul
Christophe
Jeanne Jeanne
Paul
Jean
Michel
Christophe
Anne
Julien Julien
Aurélie
Yoxu

1
requirements.txt Normal file
View file

@ -0,0 +1 @@
fuzzywuzzy==0.18.0

10
roles.txt Normal file
View file

@ -0,0 +1,10 @@
Wolf
Seer
WhiteWolf
Cupidon
Witch
Hunter
Corbeau
Stealer
Sorcier
Savior

View file

@ -1,87 +0,0 @@
# code inspiration from https://www.geekering.com/categories/computer-vision/marcellacavalcanti/hand-tracking-and-finger-counting-in-python-with-mediapipe/ because i dont know how to use mediapipe
import cv2
import mediapipe as mp
import time
mp_drawing = mp.solutions.drawing_utils
mp_drawing_styles = mp.solutions.drawing_styles
mp_hands = mp.solutions.hands
def count_fingers(stable_duration=2):
previous_finger_count = -1 # Initialize to an invalid count
stable_start_time = None # To track when the count became stable
cap = cv2.VideoCapture(0)
with mp_hands.Hands(
model_complexity=0,
min_detection_confidence=0.5,
min_tracking_confidence=0.5) as hands:
while cap.isOpened():
success, image = cap.read()
if not success:
print("Ignoring empty camera frame.")
continue
image.flags.writeable = False
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
results = hands.process(image)
image.flags.writeable = True
image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
fingerCount = 0 # Reset fingerCount for each frame
if results.multi_hand_landmarks:
for hand_landmarks in results.multi_hand_landmarks:
handIndex = results.multi_hand_landmarks.index(hand_landmarks)
handLabel = results.multi_handedness[handIndex].classification[0].label
handLandmarks = []
for landmarks in hand_landmarks.landmark:
handLandmarks.append([landmarks.x, landmarks.y])
# Count fingers
if handLabel == "Left" and handLandmarks[4][0] > handLandmarks[3][0]:
fingerCount += 1
elif handLabel == "Right" and handLandmarks[4][0] < handLandmarks[3][0]:
fingerCount += 1
if handLandmarks[8][1] < handLandmarks[6][1]: # Index finger
fingerCount += 1
if handLandmarks[12][1] < handLandmarks[10][1]: # Middle finger
fingerCount += 1
if handLandmarks[16][1] < handLandmarks[14][1]: # Ring finger
fingerCount += 1
if handLandmarks[20][1] < handLandmarks[18][1]: # Pinky
fingerCount += 1
# Draw hand landmarks
mp_drawing.draw_landmarks(
image,
hand_landmarks,
mp_hands.HAND_CONNECTIONS,
mp_drawing_styles.get_default_hand_landmarks_style(),
mp_drawing_styles.get_default_hand_connections_style())
# Display finger count
cv2.putText(image, str(fingerCount), (50, 450), cv2.FONT_HERSHEY_SIMPLEX, 3, (255, 0, 0), 10)
#Display image
cv2.imshow('Finger Count', image)
# Check if the finger count is stable
if fingerCount == previous_finger_count and fingerCount != 0:
if stable_start_time is None:
stable_start_time = time.time() # Start the timer
elif time.time() - stable_start_time >= stable_duration and fingerCount != 0:
cap.release()
cv2.destroyAllWindows()
return fingerCount # Return the stable finger count
else:
stable_start_time = None # Reset the timer if the count changes
previous_finger_count = fingerCount # Update the previous count
if cv2.waitKey(5) & 0xFF == 27:
break
return None # Return None if the loop ends without a stable count

View file

@ -1,11 +0,0 @@
Abricot
Pomme
Fraise
Melon
Orange
Framboise
Cerise
Citron
Poire
Raisin
Prune

View file

@ -1,7 +0,0 @@
fuzzywuzzy==0.18.0
opencv_python
mediapipe
piper-tts
playsound
art
pygobject

View file

@ -1,10 +0,0 @@
Werewolf
Seer
Witch
Grandchild
Hunter
Werewolf
Werewolf
Villager
Cupidon
Villager

View file

@ -1,10 +0,0 @@
FR = {
"Seer": "Voyante",
"Werewolf": "Loups-Garous",
"Witch": "Sorciere",
"Cupidon": "Cupidon",
"Savior": "Salvateur",
"Villager": "Villageois",
"Grandchild": "Petite-Fille",
"Hunter": "Chasseur"
}

View file

@ -1,502 +0,0 @@
{
"dataset": "tom",
"audio": {
"sample_rate": 44100,
"quality": "medium"
},
"espeak": {
"voice": "fr"
},
"language": {
"code": "fr_FR",
"family": "fr",
"region": "FR",
"name_native": "Français",
"name_english": "French",
"country_english": "France"
},
"inference": {
"noise_scale": 0.667,
"length_scale": 1,
"noise_w": 0.8
},
"phoneme_type": "espeak",
"phoneme_map": {},
"phoneme_id_map": {
" ": [
3
],
"!": [
4
],
"\"": [
150
],
"#": [
149
],
"$": [
2
],
"'": [
5
],
"(": [
6
],
")": [
7
],
",": [
8
],
"-": [
9
],
".": [
10
],
"0": [
130
],
"1": [
131
],
"2": [
132
],
"3": [
133
],
"4": [
134
],
"5": [
135
],
"6": [
136
],
"7": [
137
],
"8": [
138
],
"9": [
139
],
":": [
11
],
";": [
12
],
"?": [
13
],
"X": [
156
],
"^": [
1
],
"_": [
0
],
"a": [
14
],
"b": [
15
],
"c": [
16
],
"d": [
17
],
"e": [
18
],
"f": [
19
],
"g": [
154
],
"h": [
20
],
"i": [
21
],
"j": [
22
],
"k": [
23
],
"l": [
24
],
"m": [
25
],
"n": [
26
],
"o": [
27
],
"p": [
28
],
"q": [
29
],
"r": [
30
],
"s": [
31
],
"t": [
32
],
"u": [
33
],
"v": [
34
],
"w": [
35
],
"x": [
36
],
"y": [
37
],
"z": [
38
],
"æ": [
39
],
"ç": [
40
],
"ð": [
41
],
"ø": [
42
],
"ħ": [
43
],
"ŋ": [
44
],
"œ": [
45
],
"ǀ": [
46
],
"ǁ": [
47
],
"ǂ": [
48
],
"ǃ": [
49
],
"ɐ": [
50
],
"ɑ": [
51
],
"ɒ": [
52
],
"ɓ": [
53
],
"ɔ": [
54
],
"ɕ": [
55
],
"ɖ": [
56
],
"ɗ": [
57
],
"ɘ": [
58
],
"ə": [
59
],
"ɚ": [
60
],
"ɛ": [
61
],
"ɜ": [
62
],
"ɞ": [
63
],
"ɟ": [
64
],
"ɠ": [
65
],
"ɡ": [
66
],
"ɢ": [
67
],
"ɣ": [
68
],
"ɤ": [
69
],
"ɥ": [
70
],
"ɦ": [
71
],
"ɧ": [
72
],
"ɨ": [
73
],
"ɪ": [
74
],
"ɫ": [
75
],
"ɬ": [
76
],
"ɭ": [
77
],
"ɮ": [
78
],
"ɯ": [
79
],
"ɰ": [
80
],
"ɱ": [
81
],
"ɲ": [
82
],
"ɳ": [
83
],
"ɴ": [
84
],
"ɵ": [
85
],
"ɶ": [
86
],
"ɸ": [
87
],
"ɹ": [
88
],
"ɺ": [
89
],
"ɻ": [
90
],
"ɽ": [
91
],
"ɾ": [
92
],
"ʀ": [
93
],
"ʁ": [
94
],
"ʂ": [
95
],
"ʃ": [
96
],
"ʄ": [
97
],
"ʈ": [
98
],
"ʉ": [
99
],
"ʊ": [
100
],
"ʋ": [
101
],
"ʌ": [
102
],
"ʍ": [
103
],
"ʎ": [
104
],
"ʏ": [
105
],
"ʐ": [
106
],
"ʑ": [
107
],
"ʒ": [
108
],
"ʔ": [
109
],
"ʕ": [
110
],
"ʘ": [
111
],
"ʙ": [
112
],
"ʛ": [
113
],
"ʜ": [
114
],
"ʝ": [
115
],
"ʟ": [
116
],
"ʡ": [
117
],
"ʢ": [
118
],
"ʦ": [
155
],
"ʰ": [
145
],
"ʲ": [
119
],
"ˈ": [
120
],
"ˌ": [
121
],
"ː": [
122
],
"ˑ": [
123
],
"˞": [
124
],
"ˤ": [
146
],
"̃": [
141
],
"̧": [
140
],
"̩": [
144
],
"̪": [
142
],
"̯": [
143
],
"̺": [
152
],
"̻": [
153
],
"β": [
125
],
"ε": [
147
],
"θ": [
126
],
"χ": [
127
],
"ᵻ": [
128
],
"↑": [
151
],
"↓": [
148
],
"ⱱ": [
129
]
},
"num_symbols": 256,
"num_speakers": 1,
"speaker_id_map": {},
"piper_version": "1.0.0"
}