character ai
Browse files- App/TTS/Schemas.py +6 -0
- App/TTS/TTSRoutes.py +17 -9
- App/TTS/utils/CharacterAi.py +162 -0
- Dockerfile +1 -2
- requirements.txt +1 -1
App/TTS/Schemas.py
CHANGED
|
@@ -30,6 +30,12 @@ class DescriptTranscript(BaseModel):
|
|
| 30 |
file_extenstion: str = ".wav"
|
| 31 |
|
| 32 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
class PiTTSRequest(BaseModel):
|
| 34 |
text: str
|
| 35 |
voice: Optional[str]
|
|
|
|
| 30 |
file_extenstion: str = ".wav"
|
| 31 |
|
| 32 |
|
| 33 |
+
# Define the request model
|
| 34 |
+
class CharacterTTSRequest(BaseModel):
|
| 35 |
+
text: str
|
| 36 |
+
voice: Optional[str] = None
|
| 37 |
+
|
| 38 |
+
|
| 39 |
class PiTTSRequest(BaseModel):
|
| 40 |
text: str
|
| 41 |
voice: Optional[str]
|
App/TTS/TTSRoutes.py
CHANGED
|
@@ -10,16 +10,18 @@ from .Schemas import (
|
|
| 10 |
DescriptSfxRequest,
|
| 11 |
DescriptTranscript,
|
| 12 |
PiTTSRequest,
|
|
|
|
| 13 |
)
|
| 14 |
from .utils.Podcastle import PodcastleAPI
|
| 15 |
from .utils.HeyGen import HeygenAPI
|
| 16 |
-
|
| 17 |
from .utils.Descript import DescriptTTS
|
| 18 |
import os
|
| 19 |
import asyncio
|
| 20 |
-
from .utils.Pi import PiAIClient
|
| 21 |
|
| 22 |
-
from
|
|
|
|
|
|
|
| 23 |
from fastapi.responses import StreamingResponse, FileResponse
|
| 24 |
import os
|
| 25 |
import aiofiles
|
|
@@ -35,7 +37,7 @@ data = {
|
|
| 35 |
|
| 36 |
descript_tts = DescriptTTS()
|
| 37 |
heyGentts = HeygenAPI(**data)
|
| 38 |
-
pi = PiAIClient(headless=True)
|
| 39 |
|
| 40 |
|
| 41 |
@tts_router.post("/generate_tts")
|
|
@@ -97,11 +99,17 @@ async def search_id(req: StatusRequest):
|
|
| 97 |
return await tts.check_status(req)
|
| 98 |
|
| 99 |
|
| 100 |
-
@tts_router.post("/
|
| 101 |
-
async def
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
|
| 106 |
|
| 107 |
@tts_router.get("/audio/{audio_name}")
|
|
|
|
| 10 |
DescriptSfxRequest,
|
| 11 |
DescriptTranscript,
|
| 12 |
PiTTSRequest,
|
| 13 |
+
CharacterTTSRequest,
|
| 14 |
)
|
| 15 |
from .utils.Podcastle import PodcastleAPI
|
| 16 |
from .utils.HeyGen import HeygenAPI
|
| 17 |
+
from .utils.CharacterAi import CharacterTTS
|
| 18 |
from .utils.Descript import DescriptTTS
|
| 19 |
import os
|
| 20 |
import asyncio
|
|
|
|
| 21 |
|
| 22 |
+
# from .utils.Pi import PiAIClient
|
| 23 |
+
|
| 24 |
+
from fastapi import Request, HTTPException
|
| 25 |
from fastapi.responses import StreamingResponse, FileResponse
|
| 26 |
import os
|
| 27 |
import aiofiles
|
|
|
|
| 37 |
|
| 38 |
descript_tts = DescriptTTS()
|
| 39 |
heyGentts = HeygenAPI(**data)
|
| 40 |
+
# pi = PiAIClient(headless=True)
|
| 41 |
|
| 42 |
|
| 43 |
@tts_router.post("/generate_tts")
|
|
|
|
| 99 |
return await tts.check_status(req)
|
| 100 |
|
| 101 |
|
| 102 |
+
@tts_router.post("/cai_tts")
|
| 103 |
+
async def cai_tts(req: CharacterTTSRequest):
|
| 104 |
+
cai = CharacterTTS()
|
| 105 |
+
return await cai.say(req)
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
# @tts_router.post("/pi_tts")
|
| 109 |
+
# async def pi_tts(req: PiTTSRequest):
|
| 110 |
+
# if not pi.initialized:
|
| 111 |
+
# await pi.setup()
|
| 112 |
+
# return await pi.say(req.text, voice=req.voice)
|
| 113 |
|
| 114 |
|
| 115 |
@tts_router.get("/audio/{audio_name}")
|
App/TTS/utils/CharacterAi.py
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
import os
|
| 3 |
+
from characterai import aiocai
|
| 4 |
+
from typing import Optional
|
| 5 |
+
from pydantic import BaseModel
|
| 6 |
+
import aiohttp
|
| 7 |
+
|
| 8 |
+
from App.TTS.Schemas import CharacterTTSRequest
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
# class CharacterTTSRequest(BaseModel):
|
| 12 |
+
# text: str
|
| 13 |
+
# voice: Optional[str] = None
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
# Voice ID selected by the user
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class CharacterTTS:
|
| 20 |
+
TTS_ENDPOINT = "https://neo.character.ai/multimodal/api/v1/memo/replay"
|
| 21 |
+
|
| 22 |
+
def __init__(self):
|
| 23 |
+
"""
|
| 24 |
+
Initialize the CharacterTTS instance.
|
| 25 |
+
|
| 26 |
+
Args:
|
| 27 |
+
token (str): The authorization token for Character AI.
|
| 28 |
+
"""
|
| 29 |
+
self.token = "110626c85c57978218fa0066f58da72807e179d2"
|
| 30 |
+
self.client = aiocai.Client(self.token)
|
| 31 |
+
|
| 32 |
+
async def say(
|
| 33 |
+
self,
|
| 34 |
+
request: CharacterTTSRequest,
|
| 35 |
+
char_id: str = "nrXc-uQ5vtunnBTqKxB39vwNyJg2TARqD9r2wHVRO4U",
|
| 36 |
+
) -> str:
|
| 37 |
+
"""
|
| 38 |
+
Send a message to the character AI and generate TTS.
|
| 39 |
+
|
| 40 |
+
Args:
|
| 41 |
+
char_id (str): The character ID to chat with.
|
| 42 |
+
request (CharacterTTSRequest): The TTS request containing text and optional voice ID.
|
| 43 |
+
|
| 44 |
+
Returns:
|
| 45 |
+
str: The replay URL for the generated TTS audio.
|
| 46 |
+
"""
|
| 47 |
+
# Connect to the AI client and start a new chat
|
| 48 |
+
me = await self.client.get_me()
|
| 49 |
+
chat = await self.client.connect()
|
| 50 |
+
async with chat as chat_session:
|
| 51 |
+
new_chat, answer = await chat_session.new_chat(char_id, me.id)
|
| 52 |
+
chat_id = new_chat.chat_id
|
| 53 |
+
|
| 54 |
+
# Send the user's message
|
| 55 |
+
message = await chat_session.send_message(
|
| 56 |
+
char=char_id, chat_id=chat_id, text=request.text
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
# Extract necessary identifiers
|
| 60 |
+
room_id = chat_id
|
| 61 |
+
turn_id = message.turn_key.turn_id
|
| 62 |
+
primary_candidate = next(
|
| 63 |
+
(
|
| 64 |
+
c
|
| 65 |
+
for c in message.candidates
|
| 66 |
+
if c.candidate_id == message.primary_candidate_id
|
| 67 |
+
),
|
| 68 |
+
None,
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
if not primary_candidate:
|
| 72 |
+
raise Exception("Primary candidate not found in the message response.")
|
| 73 |
+
|
| 74 |
+
candidate_id = primary_candidate.candidate_id
|
| 75 |
+
voice_id = request.voice if request.voice else self.default_voice()
|
| 76 |
+
|
| 77 |
+
# Generate TTS and get the replay URL
|
| 78 |
+
replay_url = await self.generate_tts(
|
| 79 |
+
room_id, turn_id, candidate_id, voice_id
|
| 80 |
+
)
|
| 81 |
+
|
| 82 |
+
return replay_url
|
| 83 |
+
|
| 84 |
+
def default_voice(self) -> str:
|
| 85 |
+
"""
|
| 86 |
+
Return a default voice ID if none is provided.
|
| 87 |
+
|
| 88 |
+
Returns:
|
| 89 |
+
str: The default voice ID.
|
| 90 |
+
"""
|
| 91 |
+
return (
|
| 92 |
+
"0ef6c2d5-14e1-420c-a634-693c3f13fade" # Replace with your default voice ID
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
async def generate_tts(
|
| 96 |
+
self, room_id: str, turn_id: str, candidate_id: str, voice_id: str
|
| 97 |
+
) -> str:
|
| 98 |
+
"""
|
| 99 |
+
Generate TTS by making a POST request to the TTS endpoint.
|
| 100 |
+
|
| 101 |
+
Args:
|
| 102 |
+
room_id (str): The chat room ID.
|
| 103 |
+
turn_id (str): The turn ID of the message.
|
| 104 |
+
candidate_id (str): The candidate ID of the message.
|
| 105 |
+
voice_id (str): The selected voice ID.
|
| 106 |
+
|
| 107 |
+
Returns:
|
| 108 |
+
str: The replay URL for the generated TTS audio.
|
| 109 |
+
"""
|
| 110 |
+
payload = {
|
| 111 |
+
"roomId": room_id,
|
| 112 |
+
"turnId": turn_id,
|
| 113 |
+
"candidateId": candidate_id,
|
| 114 |
+
"voiceId": voice_id,
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
headers = {
|
| 118 |
+
"accept": "application/json, text/plain, */*",
|
| 119 |
+
"accept-language": "en-US,en;q=0.9",
|
| 120 |
+
"authorization": f"Token {self.token}",
|
| 121 |
+
"content-type": "application/json",
|
| 122 |
+
"origin": "https://character.ai",
|
| 123 |
+
"priority": "u=1, i",
|
| 124 |
+
"referer": "https://character.ai/",
|
| 125 |
+
"sec-ch-ua": '"Google Chrome";v="129", "Not=A?Brand";v="8", "Chromium";v="129"',
|
| 126 |
+
"sec-ch-ua-mobile": "?0",
|
| 127 |
+
"sec-ch-ua-platform": '"Windows"',
|
| 128 |
+
"sec-fetch-dest": "empty",
|
| 129 |
+
"sec-fetch-mode": "cors",
|
| 130 |
+
"sec-fetch-site": "same-site",
|
| 131 |
+
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
| 132 |
+
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
| 133 |
+
"Chrome/129.0.0.0 Safari/537.36",
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
async with aiohttp.ClientSession() as session:
|
| 137 |
+
async with session.post(
|
| 138 |
+
self.TTS_ENDPOINT, json=payload, headers=headers
|
| 139 |
+
) as response:
|
| 140 |
+
if response.status == 200:
|
| 141 |
+
data = await response.json()
|
| 142 |
+
replay_url = data.get("replayUrl")
|
| 143 |
+
if replay_url:
|
| 144 |
+
print(f"TTS generated successfully. Replay URL: {replay_url}")
|
| 145 |
+
return replay_url
|
| 146 |
+
else:
|
| 147 |
+
raise Exception("Replay URL not found in the response.")
|
| 148 |
+
else:
|
| 149 |
+
error = await response.text()
|
| 150 |
+
raise Exception(
|
| 151 |
+
f"TTS generation failed with status {response.status}: {error}"
|
| 152 |
+
)
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
# async def main():
|
| 156 |
+
# cai = CharacterTTS()
|
| 157 |
+
# x = await cai.say(CharacterTTSRequest(text="Hello, how are you?"))
|
| 158 |
+
# print(x)
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
# if __name__ == "__main__":
|
| 162 |
+
# asyncio.run(main())
|
Dockerfile
CHANGED
|
@@ -28,7 +28,6 @@ RUN apt-get update && \
|
|
| 28 |
#copy requirements
|
| 29 |
COPY requirements.txt .
|
| 30 |
RUN pip install --no-cache-dir -r requirements.txt
|
| 31 |
-
RUN playwright install-deps
|
| 32 |
|
| 33 |
|
| 34 |
# Copy the application code
|
|
@@ -37,6 +36,6 @@ USER admin
|
|
| 37 |
COPY --chown=admin . /srv
|
| 38 |
|
| 39 |
# Command to run the application
|
| 40 |
-
CMD
|
| 41 |
# Expose the server port
|
| 42 |
EXPOSE 7860
|
|
|
|
| 28 |
#copy requirements
|
| 29 |
COPY requirements.txt .
|
| 30 |
RUN pip install --no-cache-dir -r requirements.txt
|
|
|
|
| 31 |
|
| 32 |
|
| 33 |
# Copy the application code
|
|
|
|
| 36 |
COPY --chown=admin . /srv
|
| 37 |
|
| 38 |
# Command to run the application
|
| 39 |
+
CMD uvicorn App.app:app --host 0.0.0.0 --port 7860 --workers 1
|
| 40 |
# Expose the server port
|
| 41 |
EXPOSE 7860
|
requirements.txt
CHANGED
|
@@ -30,7 +30,7 @@ glfw
|
|
| 30 |
Pillow
|
| 31 |
numpy
|
| 32 |
broken-source
|
| 33 |
-
|
| 34 |
|
| 35 |
# git+https://github.com/snowby666/poe-api-wrapper.git@2c91207598ad930901cb9fb09734705056b7b6a9
|
| 36 |
# git+https://github.com/Mbonea-Mjema/ballyregan.git
|
|
|
|
| 30 |
Pillow
|
| 31 |
numpy
|
| 32 |
broken-source
|
| 33 |
+
git+https://github.com/Xtr4F/PyCharacterAI.git
|
| 34 |
|
| 35 |
# git+https://github.com/snowby666/poe-api-wrapper.git@2c91207598ad930901cb9fb09734705056b7b6a9
|
| 36 |
# git+https://github.com/Mbonea-Mjema/ballyregan.git
|