|
import asyncio |
|
import os |
|
from characterai import aiocai |
|
from typing import Optional |
|
from pydantic import BaseModel |
|
import aiohttp |
|
|
|
from App.TTS.Schemas import CharacterTTSRequest |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CharacterTTS: |
|
TTS_ENDPOINT = "https://neo.character.ai/multimodal/api/v1/memo/replay" |
|
|
|
def __init__(self): |
|
""" |
|
Initialize the CharacterTTS instance. |
|
|
|
Args: |
|
token (str): The authorization token for Character AI. |
|
""" |
|
self.token = "110626c85c57978218fa0066f58da72807e179d2" |
|
self.client = aiocai.Client(self.token) |
|
|
|
async def say( |
|
self, |
|
request: CharacterTTSRequest, |
|
char_id: str = "nrXc-uQ5vtunnBTqKxB39vwNyJg2TARqD9r2wHVRO4U", |
|
) -> str: |
|
""" |
|
Send a message to the character AI and generate TTS. |
|
|
|
Args: |
|
char_id (str): The character ID to chat with. |
|
request (CharacterTTSRequest): The TTS request containing text and optional voice ID. |
|
|
|
Returns: |
|
str: The replay URL for the generated TTS audio. |
|
""" |
|
|
|
me = await self.client.get_me() |
|
chat = await self.client.connect() |
|
async with chat as chat_session: |
|
new_chat, answer = await chat_session.new_chat(char_id, me.id) |
|
chat_id = new_chat.chat_id |
|
|
|
|
|
message = await chat_session.send_message( |
|
char=char_id, chat_id=chat_id, text=request.text |
|
) |
|
|
|
|
|
room_id = chat_id |
|
turn_id = message.turn_key.turn_id |
|
primary_candidate = next( |
|
( |
|
c |
|
for c in message.candidates |
|
if c.candidate_id == message.primary_candidate_id |
|
), |
|
None, |
|
) |
|
|
|
if not primary_candidate: |
|
raise Exception("Primary candidate not found in the message response.") |
|
|
|
candidate_id = primary_candidate.candidate_id |
|
voice_id = request.voice if request.voice else self.default_voice() |
|
|
|
|
|
replay_url = await self.generate_tts( |
|
room_id, turn_id, candidate_id, voice_id |
|
) |
|
|
|
return replay_url |
|
|
|
def default_voice(self) -> str: |
|
""" |
|
Return a default voice ID if none is provided. |
|
|
|
Returns: |
|
str: The default voice ID. |
|
""" |
|
return ( |
|
"0ef6c2d5-14e1-420c-a634-693c3f13fade" |
|
) |
|
|
|
async def generate_tts( |
|
self, room_id: str, turn_id: str, candidate_id: str, voice_id: str |
|
) -> str: |
|
""" |
|
Generate TTS by making a POST request to the TTS endpoint. |
|
|
|
Args: |
|
room_id (str): The chat room ID. |
|
turn_id (str): The turn ID of the message. |
|
candidate_id (str): The candidate ID of the message. |
|
voice_id (str): The selected voice ID. |
|
|
|
Returns: |
|
str: The replay URL for the generated TTS audio. |
|
""" |
|
payload = { |
|
"roomId": room_id, |
|
"turnId": turn_id, |
|
"candidateId": candidate_id, |
|
"voiceId": voice_id, |
|
} |
|
|
|
headers = { |
|
"accept": "application/json, text/plain, */*", |
|
"accept-language": "en-US,en;q=0.9", |
|
"authorization": f"Token {self.token}", |
|
"content-type": "application/json", |
|
"origin": "https://character.ai", |
|
"priority": "u=1, i", |
|
"referer": "https://character.ai/", |
|
"sec-ch-ua": '"Google Chrome";v="129", "Not=A?Brand";v="8", "Chromium";v="129"', |
|
"sec-ch-ua-mobile": "?0", |
|
"sec-ch-ua-platform": '"Windows"', |
|
"sec-fetch-dest": "empty", |
|
"sec-fetch-mode": "cors", |
|
"sec-fetch-site": "same-site", |
|
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " |
|
"AppleWebKit/537.36 (KHTML, like Gecko) " |
|
"Chrome/129.0.0.0 Safari/537.36", |
|
} |
|
|
|
async with aiohttp.ClientSession() as session: |
|
async with session.post( |
|
self.TTS_ENDPOINT, json=payload, headers=headers |
|
) as response: |
|
if response.status == 200: |
|
data = await response.json() |
|
replay_url = data.get("replayUrl") |
|
if replay_url: |
|
print(f"TTS generated successfully. Replay URL: {replay_url}") |
|
return replay_url |
|
else: |
|
raise Exception("Replay URL not found in the response.") |
|
else: |
|
error = await response.text() |
|
raise Exception( |
|
f"TTS generation failed with status {response.status}: {error}" |
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|