Python:
import os
import pygame
import threading
from fishaudio import FishAudio
from fishaudio.utils import save
from DLC.skills_monitor import DiInstaller
from LivinGrimoirePacket.AXPython import DrawRnd
from LivinGrimoirePacket.LivinGrimoire import Skill, Brain
from LivinGrimoirePacket.UniqueSkills import DiCMD
os.environ['PYGAME_HIDE_SUPPORT_PROMPT'] = "1"
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# β π Add a 'fish_audio' directory at the same level as main.pyβ
# β πΆ MP3 files will be created inside it automatically. β
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
class ShorniTTS:
def __init__(self, api_key: str, reference_id: str):
pygame.init()
pygame.mixer.init()
self.client = FishAudio(api_key=api_key)
self.reference_id = reference_id
self.voice = "default" # label used for filenames
@staticmethod
def __clean_filename(txt: str) -> str:
invalid_chars = ['?', ':', ',', "'", '\n', '"', '!', '.', ';']
for char in invalid_chars:
txt = txt.replace(char, "")
return txt.replace(" ", "_")
@staticmethod
def __play_file(filepath: str):
try:
sound = pygame.mixer.Sound(filepath)
sound.play()
while pygame.mixer.get_busy():
pygame.time.delay(100)
except pygame.error:
if os.path.exists(filepath):
os.remove(filepath)
def speak(self, txt: str):
file_name = self.__clean_filename(txt)
filepath = f'fish_audio/{self.voice}_{file_name}.mp3'
if len(txt) > 242:
my_thread = threading.Thread(target=self.__create_and_play_temp, args=(txt,))
elif os.path.isfile(filepath):
my_thread = threading.Thread(target=self.__play_file, args=(filepath,))
else:
my_thread = threading.Thread(target=self.__create_and_save_and_play, args=(txt,))
my_thread.daemon = True
my_thread.start()
def __create_and_play_temp(self, txt: str):
try:
audio = self.client.tts.convert(text=txt, reference_id=self.reference_id)
temp_file = f'fish_audio/temp_{hash(txt)}.mp3'
save(audio, temp_file)
if os.path.getsize(temp_file) > 0:
self.__play_file(temp_file)
os.remove(temp_file)
except Exception as e:
print(f"Error playing TTS: {e}")
def __create_and_save_and_play(self, txt: str):
try:
audio = self.client.tts.convert(text=txt, reference_id=self.reference_id)
file_name = self.__clean_filename(txt)
filepath = f'fish_audio/{self.voice}_{file_name}.mp3'
save(audio, filepath)
if os.path.getsize(filepath) > 0:
self.__play_file(filepath)
else:
os.remove(filepath)
except Exception as e:
print(f"Error creating TTS file: {e}")
def setVoice(self, newVoice: str, newReferenceId: str = None):
self.voice = newVoice
if newReferenceId:
self.reference_id = newReferenceId
class DiTTS_fish(Skill):
def __init__(self, api_key: str, reference_ids: list[str]):
super().__init__()
self.set_skill_type(3) # continuous skill
self.set_skill_lobe(2) # output(hardware) skill
self.voices: DrawRnd = DrawRnd(*reference_ids)
# pick one reference_id at random
ref_id = self.voices.renewableDraw()
self.speech: ShorniTTS = ShorniTTS(api_key=api_key, reference_id=ref_id)
self.speech.setVoice("voice")
def input(self, ear: str, skin: str, eye: str):
if self._kokoro.toHeart["cmd"] == "change voice":
new_ref = self.voices.renewableDraw()
self.speech.setVoice("voice", new_ref)
self.speech.speak("my voice has been changed")
return
if len(ear) == 0:
return
self.speech.speak(ear)
class DiTTSInstaller(DiInstaller):
def __init__(self, brain: Brain, api_key: str, reference_ids: list[str]):
super().__init__(brain)
self.skills.append(DiCMD().addModes("change voice"))
self.skills.append(DiTTS_fish(api_key, reference_ids))
def input(self, ear: str, skin: str, eye: str):
self.setSimpleAlg("Installer removed; input is unreachable.")
##########
Replace "your_api_key_here" with your Fish Audio API key.
Provide a list of reference_ids (voice IDs from Fish Audio) when instantiating DiTTSInstaller.
MP3s will now be saved in fish_audio/ instead of sounds/.