|
| 1 | +import threading, requests, base64 |
| 2 | +from playsound import playsound |
| 3 | + |
| 4 | +VOICES = [ |
| 5 | + # DISNEY VOICES |
| 6 | + 'en_us_ghostface', # Ghost Face |
| 7 | + 'en_us_chewbacca', # Chewbacca |
| 8 | + 'en_us_c3po', # C3PO |
| 9 | + 'en_us_stitch', # Stitch |
| 10 | + 'en_us_stormtrooper', # Stormtrooper |
| 11 | + 'en_us_rocket', # Rocket |
| 12 | + |
| 13 | + # ENGLISH VOICES |
| 14 | + 'en_au_001', # English AU - Female |
| 15 | + 'en_au_002', # English AU - Male |
| 16 | + 'en_uk_001', # English UK - Male 1 |
| 17 | + 'en_uk_003', # English UK - Male 2 |
| 18 | + 'en_us_001', # English US - Female (Int. 1) |
| 19 | + 'en_us_002', # English US - Female (Int. 2) |
| 20 | + 'en_us_006', # English US - Male 1 |
| 21 | + 'en_us_007', # English US - Male 2 |
| 22 | + 'en_us_009', # English US - Male 3 |
| 23 | + 'en_us_010', # English US - Male 4 |
| 24 | + |
| 25 | + # EUROPE VOICES |
| 26 | + 'fr_001', # French - Male 1 |
| 27 | + 'fr_002', # French - Male 2 |
| 28 | + 'de_001', # German - Female |
| 29 | + 'de_002', # German - Male |
| 30 | + 'es_002', # Spanish - Male |
| 31 | + |
| 32 | + # AMERICA VOICES |
| 33 | + 'es_mx_002', # Spanish MX - Male |
| 34 | + 'br_001', # Portuguese BR - Female 1 |
| 35 | + 'br_003', # Portuguese BR - Female 2 |
| 36 | + 'br_004', # Portuguese BR - Female 3 |
| 37 | + 'br_005', # Portuguese BR - Male |
| 38 | + |
| 39 | + # ASIA VOICES |
| 40 | + 'id_001', # Indonesian - Female |
| 41 | + 'jp_001', # Japanese - Female 1 |
| 42 | + 'jp_003', # Japanese - Female 2 |
| 43 | + 'jp_005', # Japanese - Female 3 |
| 44 | + 'jp_006', # Japanese - Male |
| 45 | + 'kr_002', # Korean - Male 1 |
| 46 | + 'kr_003', # Korean - Female |
| 47 | + 'kr_004', # Korean - Male 2 |
| 48 | + |
| 49 | + # SINGING VOICES |
| 50 | + 'en_female_f08_salut_damour', # Alto |
| 51 | + 'en_male_m03_lobby', # Tenor |
| 52 | + 'en_female_f08_warmy_breeze', # Warmy Breeze |
| 53 | + 'en_male_m03_sunshine_soon', # Sunshine Soon |
| 54 | + |
| 55 | + # OTHER |
| 56 | + 'en_male_narration', # narrator |
| 57 | + 'en_male_funny', # wacky |
| 58 | + 'en_female_emotional', # peaceful |
| 59 | +] |
| 60 | + |
| 61 | +ENDPOINT = 'https://tiktok-tts.weilnet.workers.dev' |
| 62 | + |
| 63 | +# in one conversion, the text can have a maximum length of 300 characters |
| 64 | +TEXT_BYTE_LIMIT = 300 |
| 65 | + |
| 66 | +# create a list by splitting a string, every element has n chars |
| 67 | +def split_string(string: str, chunk_size: int) -> list[str]: |
| 68 | + words = string.split() |
| 69 | + result = [] |
| 70 | + current_chunk = '' |
| 71 | + for word in words: |
| 72 | + if len(current_chunk) + len(word) + 1 <= chunk_size: # Check if adding the word exceeds the chunk size |
| 73 | + current_chunk += ' ' + word |
| 74 | + else: |
| 75 | + if current_chunk: # Append the current chunk if not empty |
| 76 | + result.append(current_chunk.strip()) |
| 77 | + current_chunk = word |
| 78 | + if current_chunk: # Append the last chunk if not empty |
| 79 | + result.append(current_chunk.strip()) |
| 80 | + return result |
| 81 | + |
| 82 | +# checking if the website that provides the service is available |
| 83 | +def get_api_response() -> requests.Response: |
| 84 | + url = f'{ENDPOINT}/api/status' |
| 85 | + response = requests.get(url) |
| 86 | + return response |
| 87 | + |
| 88 | +# saving the audio file |
| 89 | +def save_audio_file(base64_data: str, filename: str = "output.mp3") -> None: |
| 90 | + audio_bytes = base64.b64decode(base64_data) |
| 91 | + with open(filename, "wb") as file: |
| 92 | + file.write(audio_bytes) |
| 93 | + |
| 94 | +# send POST request to get the audio data |
| 95 | +def generate_audio(text: str, voice: str) -> bytes: |
| 96 | + url = f'{ENDPOINT}/api/generation' |
| 97 | + headers = {'Content-Type': 'application/json'} |
| 98 | + data = {'text': text, 'voice': voice} |
| 99 | + response = requests.post(url, headers=headers, json=data) |
| 100 | + return response.content |
| 101 | + |
| 102 | +# creates an text to speech audio file |
| 103 | +def tts(text: str, voice: str = "none", filename: str = "output.mp3", play_sound: bool = False) -> None: |
| 104 | + # checking if the website is available |
| 105 | + api_response = get_api_response() |
| 106 | + |
| 107 | + if api_response.status_code == 200: |
| 108 | + print("Service available!") |
| 109 | + else: |
| 110 | + print("Service not available, try again later or check, if https://tiktok-tts.weilnet.workers.dev is available...") |
| 111 | + return |
| 112 | + |
| 113 | + # checking if arguments are valid |
| 114 | + if voice == "none": |
| 115 | + print("No voice has been selected") |
| 116 | + return |
| 117 | + |
| 118 | + if not voice in VOICES: |
| 119 | + print("Voice does not exist") |
| 120 | + return |
| 121 | + |
| 122 | + if len(text) == 0: |
| 123 | + print("Insert a valid text") |
| 124 | + return |
| 125 | + |
| 126 | + # creating the audio file |
| 127 | + try: |
| 128 | + if len(text) < TEXT_BYTE_LIMIT: |
| 129 | + if len(text) < TEXT_BYTE_LIMIT: |
| 130 | + audio = generate_audio((text), voice) |
| 131 | + audio_base64_data = str(audio).split('"')[5] |
| 132 | + |
| 133 | + if audio_base64_data == "error": |
| 134 | + print("This voice is unavailable right now") |
| 135 | + return |
| 136 | + |
| 137 | + else: |
| 138 | + # Split longer text into smaller parts |
| 139 | + text_parts = split_string(text, 299) |
| 140 | + audio_base64_data = [None] * len(text_parts) |
| 141 | + |
| 142 | + # Define a thread function to generate audio for each text part |
| 143 | + def generate_audio_thread(text_part, index): |
| 144 | + audio = generate_audio(text_part, voice) |
| 145 | + base64_data = str(audio).split('"')[5] |
| 146 | + |
| 147 | + if audio_base64_data == "error": |
| 148 | + print("This voice is unavailable right now") |
| 149 | + return "error" |
| 150 | + |
| 151 | + audio_base64_data[index] = base64_data |
| 152 | + |
| 153 | + threads = [] |
| 154 | + for index, text_part in enumerate(text_parts): |
| 155 | + # Create and start a new thread for each text part |
| 156 | + thread = threading.Thread(target=generate_audio_thread, args=(text_part, index)) |
| 157 | + thread.start() |
| 158 | + threads.append(thread) |
| 159 | + |
| 160 | + # Wait for all threads to complete |
| 161 | + for thread in threads: |
| 162 | + thread.join() |
| 163 | + if (thread.result) == "error": |
| 164 | + return |
| 165 | + |
| 166 | + # Concatenate the base64 data in the correct order |
| 167 | + audio_base64_data = "".join(audio_base64_data) |
| 168 | + |
| 169 | + save_audio_file(audio_base64_data, filename) |
| 170 | + print(f"Audio file saved successfully as '{filename}'") |
| 171 | + if play_sound: |
| 172 | + playsound(filename) |
| 173 | + |
| 174 | + except Exception as e: |
| 175 | + print("Error occurred while generating audio:", str(e)) |
0 commit comments