Spaces:
Running
Running
zach
commited on
Commit
·
40403f3
1
Parent(s):
96f91bb
Update database layer and application logic for db interaction to be async
Browse files- pyproject.toml +3 -1
- src/app.py +62 -21
- src/database/__init__.py +2 -2
- src/database/crud.py +6 -6
- src/database/database.py +32 -22
- src/integrations/anthropic_api.py +0 -2
- src/scripts/init_db.py +42 -6
- src/scripts/test_db.py +55 -14
- src/utils.py +64 -72
- uv.lock +38 -15
pyproject.toml
CHANGED
@@ -6,9 +6,11 @@ readme = "README.md"
|
|
6 |
requires-python = ">=3.11"
|
7 |
dependencies = [
|
8 |
"anthropic>=0.45.2",
|
|
|
9 |
"elevenlabs>=1.50.7",
|
10 |
"gradio>=5.15.0",
|
11 |
-
"
|
|
|
12 |
"python-dotenv>=1.0.1",
|
13 |
"requests>=2.32.3",
|
14 |
"sqlalchemy>=2.0.0",
|
|
|
6 |
requires-python = ">=3.11"
|
7 |
dependencies = [
|
8 |
"anthropic>=0.45.2",
|
9 |
+
"asyncpg>=0.28.0",
|
10 |
"elevenlabs>=1.50.7",
|
11 |
"gradio>=5.15.0",
|
12 |
+
"greenlet>=2.0.0",
|
13 |
+
"httpx>=0.24.1",
|
14 |
"python-dotenv>=1.0.1",
|
15 |
"requests>=2.32.3",
|
16 |
"sqlalchemy>=2.0.0",
|
src/app.py
CHANGED
@@ -9,6 +9,8 @@ Users can compare the outputs and vote for their favorite in an interactive UI.
|
|
9 |
"""
|
10 |
|
11 |
# Standard Library Imports
|
|
|
|
|
12 |
import time
|
13 |
from concurrent.futures import ThreadPoolExecutor
|
14 |
from typing import Tuple
|
@@ -20,7 +22,7 @@ import gradio as gr
|
|
20 |
from src import constants
|
21 |
from src.config import Config, logger
|
22 |
from src.custom_types import Option, OptionMap
|
23 |
-
from src.database.database import
|
24 |
from src.integrations import (
|
25 |
AnthropicError,
|
26 |
ElevenLabsError,
|
@@ -41,9 +43,9 @@ from src.utils import (
|
|
41 |
|
42 |
class App:
|
43 |
config: Config
|
44 |
-
db_session_maker:
|
45 |
|
46 |
-
def __init__(self, config: Config, db_session_maker:
|
47 |
self.config = config
|
48 |
self.db_session_maker = db_session_maker
|
49 |
|
@@ -188,6 +190,47 @@ class App:
|
|
188 |
logger.error(f"Unexpected error during TTS generation: {e}")
|
189 |
raise gr.Error("An unexpected error occurred. Please try again later.")
|
190 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
191 |
def _vote(
|
192 |
self,
|
193 |
vote_submitted: bool,
|
@@ -203,19 +246,14 @@ class App:
|
|
203 |
Args:
|
204 |
vote_submitted (bool): True if a vote was already submitted.
|
205 |
option_map (OptionMap): A dictionary mapping option labels to their details.
|
206 |
-
Expected structure:
|
207 |
-
{
|
208 |
-
'Option A': 'Hume AI',
|
209 |
-
'Option B': 'ElevenLabs',
|
210 |
-
}
|
211 |
clicked_option_button (str): The button that was clicked.
|
212 |
|
213 |
Returns:
|
214 |
A tuple of:
|
215 |
-
|
216 |
-
|
217 |
-
|
218 |
-
|
219 |
"""
|
220 |
if not option_map or vote_submitted:
|
221 |
return gr.skip(), gr.skip(), gr.skip(), gr.skip()
|
@@ -224,16 +262,19 @@ class App:
|
|
224 |
selected_provider = option_map[selected_option]["provider"]
|
225 |
other_provider = option_map[other_option]["provider"]
|
226 |
|
227 |
-
#
|
228 |
-
|
229 |
-
|
230 |
-
|
231 |
-
|
232 |
-
|
233 |
-
|
234 |
-
|
235 |
-
|
|
|
|
|
236 |
)
|
|
|
237 |
|
238 |
# Build button text, displaying the provider and voice name, appending the trophy emoji to the selected option.
|
239 |
selected_label = f"{selected_provider} {constants.TROPHY_EMOJI}"
|
|
|
9 |
"""
|
10 |
|
11 |
# Standard Library Imports
|
12 |
+
import asyncio
|
13 |
+
import threading
|
14 |
import time
|
15 |
from concurrent.futures import ThreadPoolExecutor
|
16 |
from typing import Tuple
|
|
|
22 |
from src import constants
|
23 |
from src.config import Config, logger
|
24 |
from src.custom_types import Option, OptionMap
|
25 |
+
from src.database.database import AsyncDBSessionMaker
|
26 |
from src.integrations import (
|
27 |
AnthropicError,
|
28 |
ElevenLabsError,
|
|
|
43 |
|
44 |
class App:
|
45 |
config: Config
|
46 |
+
db_session_maker: AsyncDBSessionMaker
|
47 |
|
48 |
+
def __init__(self, config: Config, db_session_maker: AsyncDBSessionMaker):
|
49 |
self.config = config
|
50 |
self.db_session_maker = db_session_maker
|
51 |
|
|
|
190 |
logger.error(f"Unexpected error during TTS generation: {e}")
|
191 |
raise gr.Error("An unexpected error occurred. Please try again later.")
|
192 |
|
193 |
+
|
194 |
+
def _background_submit_vote(
|
195 |
+
self,
|
196 |
+
option_map: OptionMap,
|
197 |
+
selected_option: constants.OptionKey,
|
198 |
+
text_modified: bool,
|
199 |
+
character_description: str,
|
200 |
+
text: str,
|
201 |
+
) -> None:
|
202 |
+
"""
|
203 |
+
Runs the vote submission in a background thread.
|
204 |
+
Creates a new event loop and runs the async submit_voting_results function in it.
|
205 |
+
|
206 |
+
Args:
|
207 |
+
Same as submit_voting_results
|
208 |
+
|
209 |
+
Returns:
|
210 |
+
None
|
211 |
+
"""
|
212 |
+
try:
|
213 |
+
# Create a new event loop for this thread
|
214 |
+
loop = asyncio.new_event_loop()
|
215 |
+
asyncio.set_event_loop(loop)
|
216 |
+
|
217 |
+
# Run the async function in the new loop
|
218 |
+
loop.run_until_complete(submit_voting_results(
|
219 |
+
option_map,
|
220 |
+
selected_option,
|
221 |
+
text_modified,
|
222 |
+
character_description,
|
223 |
+
text,
|
224 |
+
self.db_session_maker,
|
225 |
+
self.config,
|
226 |
+
))
|
227 |
+
except Exception as e:
|
228 |
+
logger.error(f"Error in background vote submission thread: {e}", exc_info=True)
|
229 |
+
finally:
|
230 |
+
# Close the loop when done
|
231 |
+
loop.close()
|
232 |
+
|
233 |
+
|
234 |
def _vote(
|
235 |
self,
|
236 |
vote_submitted: bool,
|
|
|
246 |
Args:
|
247 |
vote_submitted (bool): True if a vote was already submitted.
|
248 |
option_map (OptionMap): A dictionary mapping option labels to their details.
|
|
|
|
|
|
|
|
|
|
|
249 |
clicked_option_button (str): The button that was clicked.
|
250 |
|
251 |
Returns:
|
252 |
A tuple of:
|
253 |
+
- A boolean indicating if the vote was accepted.
|
254 |
+
- A dict update for the selected vote button (showing provider and trophy emoji).
|
255 |
+
- A dict update for the unselected vote button (showing provider).
|
256 |
+
- A dict update for enabling vote interactions.
|
257 |
"""
|
258 |
if not option_map or vote_submitted:
|
259 |
return gr.skip(), gr.skip(), gr.skip(), gr.skip()
|
|
|
262 |
selected_provider = option_map[selected_option]["provider"]
|
263 |
other_provider = option_map[other_option]["provider"]
|
264 |
|
265 |
+
# Start a background thread for the database operation
|
266 |
+
thread = threading.Thread(
|
267 |
+
target=self._background_submit_vote,
|
268 |
+
args=(
|
269 |
+
option_map,
|
270 |
+
selected_option,
|
271 |
+
text_modified,
|
272 |
+
character_description,
|
273 |
+
text,
|
274 |
+
),
|
275 |
+
daemon=True
|
276 |
)
|
277 |
+
thread.start()
|
278 |
|
279 |
# Build button text, displaying the provider and voice name, appending the trophy emoji to the selected option.
|
280 |
selected_label = f"{selected_provider} {constants.TROPHY_EMOJI}"
|
src/database/__init__.py
CHANGED
@@ -1,9 +1,9 @@
|
|
1 |
from .crud import create_vote
|
2 |
-
from .database import
|
3 |
|
4 |
__all__ = [
|
|
|
5 |
"Base",
|
6 |
-
"DBSessionMaker",
|
7 |
"create_vote",
|
8 |
"engine",
|
9 |
"init_db",
|
|
|
1 |
from .crud import create_vote
|
2 |
+
from .database import AsyncDBSessionMaker, Base, engine, init_db
|
3 |
|
4 |
__all__ = [
|
5 |
+
"AsyncDBSessionMaker",
|
6 |
"Base",
|
|
|
7 |
"create_vote",
|
8 |
"engine",
|
9 |
"init_db",
|
src/database/crud.py
CHANGED
@@ -6,19 +6,19 @@ Since vote records are never updated or deleted, only functions to create and re
|
|
6 |
"""
|
7 |
|
8 |
# Third-Party Library Imports
|
9 |
-
from sqlalchemy.
|
10 |
|
11 |
# Local Application Imports
|
12 |
from src.custom_types import VotingResults
|
13 |
from src.database.models import VoteResult
|
14 |
|
15 |
|
16 |
-
def create_vote(db:
|
17 |
"""
|
18 |
Create a new vote record in the database based on the given VotingResults data.
|
19 |
|
20 |
Args:
|
21 |
-
db (
|
22 |
vote_data (VotingResults): The vote data to persist.
|
23 |
|
24 |
Returns:
|
@@ -38,9 +38,9 @@ def create_vote(db: Session, vote_data: VotingResults) -> VoteResult:
|
|
38 |
)
|
39 |
db.add(vote)
|
40 |
try:
|
41 |
-
db.commit()
|
42 |
except Exception as e:
|
43 |
-
db.rollback()
|
44 |
raise e
|
45 |
-
db.refresh(vote)
|
46 |
return vote
|
|
|
6 |
"""
|
7 |
|
8 |
# Third-Party Library Imports
|
9 |
+
from sqlalchemy.ext.asyncio import AsyncSession
|
10 |
|
11 |
# Local Application Imports
|
12 |
from src.custom_types import VotingResults
|
13 |
from src.database.models import VoteResult
|
14 |
|
15 |
|
16 |
+
async def create_vote(db: AsyncSession, vote_data: VotingResults) -> VoteResult:
|
17 |
"""
|
18 |
Create a new vote record in the database based on the given VotingResults data.
|
19 |
|
20 |
Args:
|
21 |
+
db (AsyncSession): The SQLAlchemy async database session.
|
22 |
vote_data (VotingResults): The vote data to persist.
|
23 |
|
24 |
Returns:
|
|
|
38 |
)
|
39 |
db.add(vote)
|
40 |
try:
|
41 |
+
await db.commit()
|
42 |
except Exception as e:
|
43 |
+
await db.rollback()
|
44 |
raise e
|
45 |
+
await db.refresh(vote)
|
46 |
return vote
|
src/database/database.py
CHANGED
@@ -12,8 +12,8 @@ If no DATABASE_URL environment variable is set, then create a dummy database to
|
|
12 |
from typing import Callable, Optional
|
13 |
|
14 |
# Third-Party Library Imports
|
15 |
-
from sqlalchemy import
|
16 |
-
from sqlalchemy.orm import DeclarativeBase
|
17 |
|
18 |
# Local Application Imports
|
19 |
from src.config import Config
|
@@ -24,44 +24,44 @@ class Base(DeclarativeBase):
|
|
24 |
pass
|
25 |
|
26 |
|
27 |
-
engine: Optional[
|
28 |
|
29 |
|
30 |
-
class
|
31 |
is_dummy = True # Flag to indicate this is a dummy session.
|
32 |
|
33 |
-
def __enter__(self):
|
34 |
return self
|
35 |
|
36 |
-
def __exit__(self, exc_type, exc_value, traceback):
|
37 |
pass
|
38 |
|
39 |
-
def add(self, _instance, _warn=True):
|
40 |
# No-op: simply ignore adding the instance.
|
41 |
pass
|
42 |
|
43 |
-
def commit(self):
|
44 |
# Raise an exception to simulate failure when attempting a write.
|
45 |
raise RuntimeError("DummySession does not support commit operations.")
|
46 |
|
47 |
-
def refresh(self, _instance):
|
48 |
# Raise an exception to simulate failure when attempting to refresh.
|
49 |
raise RuntimeError("DummySession does not support refresh operations.")
|
50 |
|
51 |
-
def rollback(self):
|
52 |
# No-op: there's nothing to roll back.
|
53 |
pass
|
54 |
|
55 |
-
def close(self):
|
56 |
# No-op: nothing to close.
|
57 |
pass
|
58 |
|
59 |
|
60 |
-
#
|
61 |
-
|
62 |
|
63 |
|
64 |
-
def init_db(config: Config) ->
|
65 |
"""
|
66 |
Initialize the database engine and return a session factory based on the provided configuration.
|
67 |
|
@@ -72,27 +72,37 @@ def init_db(config: Config) -> DBSessionMaker:
|
|
72 |
config (Config): The application configuration.
|
73 |
|
74 |
Returns:
|
75 |
-
|
76 |
"""
|
77 |
# ruff doesn't like setting global variables, but this is practical here
|
78 |
global engine # noqa
|
79 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
80 |
if config.app_env == "prod":
|
81 |
# In production, a valid DATABASE_URL is required.
|
82 |
if not config.database_url:
|
83 |
raise ValueError("DATABASE_URL must be set in production!")
|
84 |
-
|
85 |
-
|
|
|
86 |
|
87 |
# In development, if a DATABASE_URL is provided, use it.
|
88 |
if config.database_url:
|
89 |
-
|
90 |
-
|
|
|
91 |
|
92 |
# No DATABASE_URL is provided; use a DummySession that does nothing.
|
93 |
engine = None
|
94 |
|
95 |
-
def
|
96 |
-
return
|
|
|
|
|
97 |
|
98 |
-
return dummy_session_factory
|
|
|
12 |
from typing import Callable, Optional
|
13 |
|
14 |
# Third-Party Library Imports
|
15 |
+
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker, create_async_engine
|
16 |
+
from sqlalchemy.orm import DeclarativeBase
|
17 |
|
18 |
# Local Application Imports
|
19 |
from src.config import Config
|
|
|
24 |
pass
|
25 |
|
26 |
|
27 |
+
engine: Optional[AsyncEngine] = None
|
28 |
|
29 |
|
30 |
+
class AsyncDummySession:
|
31 |
is_dummy = True # Flag to indicate this is a dummy session.
|
32 |
|
33 |
+
async def __enter__(self):
|
34 |
return self
|
35 |
|
36 |
+
async def __exit__(self, exc_type, exc_value, traceback):
|
37 |
pass
|
38 |
|
39 |
+
async def add(self, _instance, _warn=True):
|
40 |
# No-op: simply ignore adding the instance.
|
41 |
pass
|
42 |
|
43 |
+
async def commit(self):
|
44 |
# Raise an exception to simulate failure when attempting a write.
|
45 |
raise RuntimeError("DummySession does not support commit operations.")
|
46 |
|
47 |
+
async def refresh(self, _instance):
|
48 |
# Raise an exception to simulate failure when attempting to refresh.
|
49 |
raise RuntimeError("DummySession does not support refresh operations.")
|
50 |
|
51 |
+
async def rollback(self):
|
52 |
# No-op: there's nothing to roll back.
|
53 |
pass
|
54 |
|
55 |
+
async def close(self):
|
56 |
# No-op: nothing to close.
|
57 |
pass
|
58 |
|
59 |
|
60 |
+
# AsyncDBSessionMaker is either a async_sessionmaker instance or a callable that returns a AsyncDummySession.
|
61 |
+
AsyncDBSessionMaker = async_sessionmaker | Callable[[], AsyncDummySession]
|
62 |
|
63 |
|
64 |
+
def init_db(config: Config) -> AsyncDBSessionMaker:
|
65 |
"""
|
66 |
Initialize the database engine and return a session factory based on the provided configuration.
|
67 |
|
|
|
72 |
config (Config): The application configuration.
|
73 |
|
74 |
Returns:
|
75 |
+
AsyncDBSessionMaker: A sessionmaker bound to the engine, or a dummy session factory.
|
76 |
"""
|
77 |
# ruff doesn't like setting global variables, but this is practical here
|
78 |
global engine # noqa
|
79 |
|
80 |
+
# Convert standard PostgreSQL URL to async format
|
81 |
+
def convert_to_async_url(url: str) -> str:
|
82 |
+
# Convert postgresql:// to postgresql+asyncpg://
|
83 |
+
if url.startswith('postgresql://'):
|
84 |
+
return url.replace('postgresql://', 'postgresql+asyncpg://', 1)
|
85 |
+
return url
|
86 |
+
|
87 |
if config.app_env == "prod":
|
88 |
# In production, a valid DATABASE_URL is required.
|
89 |
if not config.database_url:
|
90 |
raise ValueError("DATABASE_URL must be set in production!")
|
91 |
+
async_db_url = convert_to_async_url(config.database_url)
|
92 |
+
engine = create_async_engine(async_db_url)
|
93 |
+
return async_sessionmaker(bind=engine, expire_on_commit=False, class_=AsyncSession)
|
94 |
|
95 |
# In development, if a DATABASE_URL is provided, use it.
|
96 |
if config.database_url:
|
97 |
+
async_db_url = convert_to_async_url(config.database_url)
|
98 |
+
engine = create_async_engine(async_db_url)
|
99 |
+
return async_sessionmaker(bind=engine, expire_on_commit=False, class_=AsyncSession)
|
100 |
|
101 |
# No DATABASE_URL is provided; use a DummySession that does nothing.
|
102 |
engine = None
|
103 |
|
104 |
+
def async_dummy_session_factory() -> AsyncDummySession:
|
105 |
+
return AsyncDummySession()
|
106 |
+
|
107 |
+
return async_dummy_session_factory
|
108 |
|
|
src/integrations/anthropic_api.py
CHANGED
@@ -44,8 +44,6 @@ Your absolute priority is delivering complete, untruncated responses within stri
|
|
44 |
words. NEVER include newlines in the output
|
45 |
- Make sure that all responses are complete thoughts, not fragments, and have clear beginnings and endings
|
46 |
- The text must sound human-like, prosodic, expressive, conversational. Avoid generic AI-like words like "delve".
|
47 |
-
- Use the utterances "uh", "um", "hm", "woah", or "like" for expressivity in conversational text. Use these naturally
|
48 |
-
within the sentence. Never use them at the very end of a sentence.
|
49 |
- Avoid any short utterances at the end of the sentence - like ", hm?" or "oh" at the end. Avoid these short, isolated
|
50 |
utterances because they are difficult for our TTS system to speak.
|
51 |
- Avoid words that are overly long, very rare, or difficult to pronounce. For example, avoid "eureka", or "schnell",
|
|
|
44 |
words. NEVER include newlines in the output
|
45 |
- Make sure that all responses are complete thoughts, not fragments, and have clear beginnings and endings
|
46 |
- The text must sound human-like, prosodic, expressive, conversational. Avoid generic AI-like words like "delve".
|
|
|
|
|
47 |
- Avoid any short utterances at the end of the sentence - like ", hm?" or "oh" at the end. Avoid these short, isolated
|
48 |
utterances because they are difficult for our TTS system to speak.
|
49 |
- Avoid words that are overly long, very rare, or difficult to pronounce. For example, avoid "eureka", or "schnell",
|
src/scripts/init_db.py
CHANGED
@@ -2,19 +2,55 @@
|
|
2 |
init_db.py
|
3 |
|
4 |
This script initializes the database by creating all tables defined in the ORM models.
|
5 |
-
|
|
|
6 |
"""
|
7 |
|
|
|
|
|
|
|
8 |
# Local Application Imports
|
9 |
-
from src.config import logger
|
10 |
from src.database.database import engine
|
11 |
from src.database.models import Base
|
12 |
|
13 |
|
14 |
-
def
|
15 |
-
|
16 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
17 |
|
18 |
|
19 |
if __name__ == "__main__":
|
20 |
-
|
|
|
2 |
init_db.py
|
3 |
|
4 |
This script initializes the database by creating all tables defined in the ORM models.
|
5 |
+
It uses async SQLAlchemy operations to create tables in the PostgreSQL database.
|
6 |
+
Run this script once to set up your database schema.
|
7 |
"""
|
8 |
|
9 |
+
# Standard Library Imports
|
10 |
+
import asyncio
|
11 |
+
|
12 |
# Local Application Imports
|
13 |
+
from src.config import Config, logger
|
14 |
from src.database.database import engine
|
15 |
from src.database.models import Base
|
16 |
|
17 |
|
18 |
+
async def init_db_async():
|
19 |
+
"""
|
20 |
+
Asynchronously create all database tables defined in the ORM models.
|
21 |
+
|
22 |
+
This function connects to the database using the configured async engine
|
23 |
+
and creates all tables that are mapped to SQLAlchemy models derived from
|
24 |
+
the Base class. It uses SQLAlchemy's create_all method with the async
|
25 |
+
engine context.
|
26 |
+
|
27 |
+
Returns:
|
28 |
+
None
|
29 |
+
"""
|
30 |
+
async with engine.begin() as conn:
|
31 |
+
# In SQLAlchemy 2.0 with async, we use the connection directly
|
32 |
+
await conn.run_sync(Base.metadata.create_all)
|
33 |
+
|
34 |
+
logger.info("Database tables created successfully using async SQLAlchemy.")
|
35 |
+
|
36 |
+
|
37 |
+
def main():
|
38 |
+
"""
|
39 |
+
Main entry point for the database initialization script.
|
40 |
+
|
41 |
+
This function creates the configuration, ensures the async engine is
|
42 |
+
initialized, and runs the async initialization function within an
|
43 |
+
event loop.
|
44 |
+
|
45 |
+
Returns:
|
46 |
+
None
|
47 |
+
"""
|
48 |
+
# Make sure config is loaded first to initialize the engine
|
49 |
+
Config.get()
|
50 |
+
|
51 |
+
# Run the async initialization function
|
52 |
+
asyncio.run(init_db_async())
|
53 |
|
54 |
|
55 |
if __name__ == "__main__":
|
56 |
+
main()
|
src/scripts/test_db.py
CHANGED
@@ -2,11 +2,12 @@
|
|
2 |
test_db.py
|
3 |
|
4 |
This script verifies the database connection for the Expressive TTS Arena project.
|
5 |
-
It attempts to connect to the PostgreSQL database using SQLAlchemy and executes
|
|
|
6 |
|
7 |
Functionality:
|
8 |
- Loads the database connection from `database.py`.
|
9 |
-
- Attempts to establish
|
10 |
- Executes a test query (`SELECT 1`) to confirm connectivity.
|
11 |
- Prints a success message if the connection is valid.
|
12 |
- Prints an error message if the connection fails.
|
@@ -22,33 +23,73 @@ Troubleshooting:
|
|
22 |
- Ensure the `.env` file contains a valid `DATABASE_URL`.
|
23 |
- Check that the database server is running and accessible.
|
24 |
- Verify PostgreSQL credentials and network settings.
|
25 |
-
|
26 |
"""
|
27 |
|
28 |
# Standard Library Imports
|
|
|
29 |
import sys
|
30 |
|
31 |
# Third-Party Library Imports
|
32 |
from sqlalchemy import text
|
33 |
-
from sqlalchemy.exc import OperationalError
|
34 |
|
35 |
# Local Application Imports
|
36 |
-
from src.config import logger
|
37 |
-
from src.database import engine
|
|
|
|
|
|
|
|
|
|
|
38 |
|
|
|
|
|
|
|
39 |
|
40 |
-
|
|
|
|
|
41 |
if engine is None:
|
42 |
logger.error("No valid database engine configured.")
|
43 |
-
|
44 |
|
45 |
try:
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
52 |
|
53 |
|
54 |
if __name__ == "__main__":
|
|
|
2 |
test_db.py
|
3 |
|
4 |
This script verifies the database connection for the Expressive TTS Arena project.
|
5 |
+
It attempts to connect to the PostgreSQL database using async SQLAlchemy and executes
|
6 |
+
a simple query to confirm connectivity.
|
7 |
|
8 |
Functionality:
|
9 |
- Loads the database connection from `database.py`.
|
10 |
+
- Attempts to establish an async connection to the database.
|
11 |
- Executes a test query (`SELECT 1`) to confirm connectivity.
|
12 |
- Prints a success message if the connection is valid.
|
13 |
- Prints an error message if the connection fails.
|
|
|
23 |
- Ensure the `.env` file contains a valid `DATABASE_URL`.
|
24 |
- Check that the database server is running and accessible.
|
25 |
- Verify PostgreSQL credentials and network settings.
|
|
|
26 |
"""
|
27 |
|
28 |
# Standard Library Imports
|
29 |
+
import asyncio
|
30 |
import sys
|
31 |
|
32 |
# Third-Party Library Imports
|
33 |
from sqlalchemy import text
|
|
|
34 |
|
35 |
# Local Application Imports
|
36 |
+
from src.config import Config, logger
|
37 |
+
from src.database.database import engine, init_db
|
38 |
+
|
39 |
+
|
40 |
+
async def test_connection_async():
|
41 |
+
"""
|
42 |
+
Asynchronously test the database connection.
|
43 |
|
44 |
+
This function attempts to connect to the database using the configured
|
45 |
+
async engine and execute a simple SELECT query. It logs success or failure
|
46 |
+
messages accordingly.
|
47 |
|
48 |
+
Returns:
|
49 |
+
bool: True if the connection was successful, False otherwise.
|
50 |
+
"""
|
51 |
if engine is None:
|
52 |
logger.error("No valid database engine configured.")
|
53 |
+
return False
|
54 |
|
55 |
try:
|
56 |
+
# Create a new async session
|
57 |
+
async with engine.connect() as conn:
|
58 |
+
# Execute a simple query to verify connectivity
|
59 |
+
result = await conn.execute(text("SELECT 1"))
|
60 |
+
# Fetch the result to make sure the query completes
|
61 |
+
await result.fetchone()
|
62 |
+
|
63 |
+
logger.info("Async database connection successful!")
|
64 |
+
return True
|
65 |
+
|
66 |
+
except Exception as e:
|
67 |
+
logger.error(f"Async database connection failed: {e}")
|
68 |
+
return False
|
69 |
+
|
70 |
+
|
71 |
+
def main():
|
72 |
+
"""
|
73 |
+
Main entry point for the database connection test script.
|
74 |
+
|
75 |
+
This function creates the configuration, initializes the database engine,
|
76 |
+
and runs the async test function within an event loop. It exits with an
|
77 |
+
appropriate system exit code based on the test result.
|
78 |
+
|
79 |
+
Returns:
|
80 |
+
None
|
81 |
+
"""
|
82 |
+
# Make sure config is loaded first to initialize the engine
|
83 |
+
config = Config.get()
|
84 |
+
|
85 |
+
# Initialize the database engine
|
86 |
+
init_db(config)
|
87 |
+
|
88 |
+
# Run the async test function
|
89 |
+
success = asyncio.run(test_connection_async())
|
90 |
+
|
91 |
+
# Exit with an appropriate status code
|
92 |
+
sys.exit(0 if success else 1)
|
93 |
|
94 |
|
95 |
if __name__ == "__main__":
|
src/utils.py
CHANGED
@@ -14,7 +14,8 @@ import time
|
|
14 |
from pathlib import Path
|
15 |
from typing import Tuple, cast
|
16 |
|
17 |
-
|
|
|
18 |
|
19 |
# Local Application Imports
|
20 |
from src import constants
|
@@ -28,7 +29,7 @@ from src.custom_types import (
|
|
28 |
VotingResults,
|
29 |
)
|
30 |
from src.database import crud
|
31 |
-
from src.database.database import
|
32 |
|
33 |
|
34 |
def truncate_text(text: str, max_length: int = 50) -> str:
|
@@ -301,107 +302,98 @@ def _log_voting_results(voting_results: VotingResults) -> None:
|
|
301 |
logger.info("Voting results:\n%s", json.dumps(voting_results, indent=4))
|
302 |
|
303 |
|
304 |
-
def
|
305 |
-
|
306 |
voting_results: VotingResults,
|
307 |
-
|
308 |
-
config: Config,
|
309 |
) -> None:
|
310 |
"""
|
311 |
-
|
312 |
-
|
313 |
-
In production (or in dev with a real session):
|
314 |
-
- Logs the error (with full traceback in prod) and the voting results.
|
315 |
-
- In production, re-raises the exception.
|
316 |
-
|
317 |
-
In development with a dummy session:
|
318 |
-
- Only logs the voting results.
|
319 |
-
"""
|
320 |
-
|
321 |
-
if config.app_env == "prod" or (config.app_env == "dev" and not is_dummy_db_session):
|
322 |
-
logger.error("Failed to create vote record: %s", e, exc_info=(config.app_env == "prod"))
|
323 |
-
_log_voting_results(voting_results)
|
324 |
-
if config.app_env == "prod":
|
325 |
-
raise e
|
326 |
-
else:
|
327 |
-
# Dev mode with a dummy session: only log the voting results.
|
328 |
-
_log_voting_results(voting_results)
|
329 |
-
|
330 |
-
|
331 |
-
def _persist_vote(db_session_maker: DBSessionMaker, voting_results: VotingResults, config: Config) -> None:
|
332 |
-
"""
|
333 |
-
Persist a vote record in the database and handle potential failures.
|
334 |
-
|
335 |
-
This function obtains a database session using the provided session maker and attempts
|
336 |
-
to create a vote record using the specified voting results. If the session is identified
|
337 |
-
as a dummy session, it logs a success message and outputs the voting results. If an error
|
338 |
-
occurs during vote creation, the function delegates error handling to _handle_vote_failure.
|
339 |
-
On successful vote creation, it logs the success and, when running in a development environment,
|
340 |
-
logs the full voting results for debugging purposes. In all cases, the database session is
|
341 |
-
properly closed after the operation.
|
342 |
|
343 |
Args:
|
344 |
-
db_session_maker (
|
345 |
voting_results (VotingResults): A dictionary containing the details of the vote to persist.
|
346 |
config (Config): The application configuration, used to determine environment-specific behavior.
|
|
|
|
|
|
|
347 |
"""
|
|
|
|
|
|
|
348 |
|
349 |
-
|
350 |
-
is_dummy_db_session = getattr(db, "is_dummy", False)
|
351 |
-
if is_dummy_db_session:
|
352 |
logger.info("Vote record created successfully.")
|
353 |
_log_voting_results(voting_results)
|
|
|
|
|
|
|
354 |
try:
|
355 |
-
crud.create_vote(cast(
|
356 |
-
except Exception as e:
|
357 |
-
_handle_vote_failure(e, voting_results, is_dummy_db_session, config)
|
358 |
-
else:
|
359 |
logger.info("Vote record created successfully.")
|
360 |
if config.app_env == "dev":
|
361 |
_log_voting_results(voting_results)
|
|
|
|
|
|
|
|
|
|
|
362 |
finally:
|
363 |
-
|
|
|
364 |
|
365 |
|
366 |
-
def submit_voting_results(
|
367 |
option_map: OptionMap,
|
368 |
selected_option: OptionKey,
|
369 |
text_modified: bool,
|
370 |
character_description: str,
|
371 |
text: str,
|
372 |
-
db_session_maker:
|
373 |
config: Config,
|
374 |
) -> None:
|
375 |
"""
|
376 |
-
|
377 |
-
|
378 |
|
379 |
Args:
|
380 |
option_map (OptionMap): Mapping of comparison data and TTS options.
|
381 |
-
selected_option (
|
382 |
-
text_modified (bool): Indicates whether the text was modified.
|
383 |
-
character_description (str): Description of the voice/character.
|
384 |
-
text (str): The text
|
385 |
-
|
|
|
386 |
|
387 |
-
|
388 |
-
|
389 |
-
|
390 |
-
|
391 |
-
|
392 |
-
"
|
393 |
-
|
394 |
-
|
395 |
-
|
396 |
-
|
397 |
-
|
398 |
-
|
399 |
-
|
400 |
-
|
401 |
-
|
402 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
403 |
|
404 |
-
|
|
|
|
|
|
|
405 |
|
406 |
|
407 |
def validate_env_var(var_name: str) -> str:
|
|
|
14 |
from pathlib import Path
|
15 |
from typing import Tuple, cast
|
16 |
|
17 |
+
# Third-Party Library Imports
|
18 |
+
from sqlalchemy.ext.asyncio import AsyncSession
|
19 |
|
20 |
# Local Application Imports
|
21 |
from src import constants
|
|
|
29 |
VotingResults,
|
30 |
)
|
31 |
from src.database import crud
|
32 |
+
from src.database.database import AsyncDBSessionMaker
|
33 |
|
34 |
|
35 |
def truncate_text(text: str, max_length: int = 50) -> str:
|
|
|
302 |
logger.info("Voting results:\n%s", json.dumps(voting_results, indent=4))
|
303 |
|
304 |
|
305 |
+
async def _persist_vote(
|
306 |
+
db_session_maker: AsyncDBSessionMaker,
|
307 |
voting_results: VotingResults,
|
308 |
+
config: Config
|
|
|
309 |
) -> None:
|
310 |
"""
|
311 |
+
Asynchronously persist a vote record in the database and handle potential failures.
|
312 |
+
Designed to work safely in a background task context.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
313 |
|
314 |
Args:
|
315 |
+
db_session_maker (AsyncDBSessionMaker): A callable that returns a new async database session.
|
316 |
voting_results (VotingResults): A dictionary containing the details of the vote to persist.
|
317 |
config (Config): The application configuration, used to determine environment-specific behavior.
|
318 |
+
|
319 |
+
Returns:
|
320 |
+
None
|
321 |
"""
|
322 |
+
# Create session
|
323 |
+
session = db_session_maker()
|
324 |
+
is_dummy_session = getattr(session, "is_dummy", False)
|
325 |
|
326 |
+
if is_dummy_session:
|
|
|
|
|
327 |
logger.info("Vote record created successfully.")
|
328 |
_log_voting_results(voting_results)
|
329 |
+
await session.close()
|
330 |
+
return
|
331 |
+
|
332 |
try:
|
333 |
+
await crud.create_vote(cast(AsyncSession, session), voting_results)
|
|
|
|
|
|
|
334 |
logger.info("Vote record created successfully.")
|
335 |
if config.app_env == "dev":
|
336 |
_log_voting_results(voting_results)
|
337 |
+
except Exception as e:
|
338 |
+
# Log the error with traceback in production, without traceback in dev
|
339 |
+
logger.error(f"Failed to create vote record: {e}",
|
340 |
+
exc_info=(config.app_env == "prod"))
|
341 |
+
_log_voting_results(voting_results)
|
342 |
finally:
|
343 |
+
# Always ensure the session is closed
|
344 |
+
await session.close()
|
345 |
|
346 |
|
347 |
+
async def submit_voting_results(
|
348 |
option_map: OptionMap,
|
349 |
selected_option: OptionKey,
|
350 |
text_modified: bool,
|
351 |
character_description: str,
|
352 |
text: str,
|
353 |
+
db_session_maker: AsyncDBSessionMaker,
|
354 |
config: Config,
|
355 |
) -> None:
|
356 |
"""
|
357 |
+
Asynchronously constructs the voting results dictionary and persists a new vote record.
|
358 |
+
Designed to run as a background task, handling all exceptions internally.
|
359 |
|
360 |
Args:
|
361 |
option_map (OptionMap): Mapping of comparison data and TTS options.
|
362 |
+
selected_option (OptionKey): The option selected by the user.
|
363 |
+
text_modified (bool): Indicates whether the text was modified from the original generated text.
|
364 |
+
character_description (str): Description of the voice/character used for TTS generation.
|
365 |
+
text (str): The text that was synthesized into speech.
|
366 |
+
db_session_maker (AsyncDBSessionMaker): Factory function for creating async database sessions.
|
367 |
+
config (Config): Application configuration containing environment settings.
|
368 |
|
369 |
+
Returns:
|
370 |
+
None
|
371 |
+
"""
|
372 |
+
try:
|
373 |
+
provider_a: TTSProviderName = option_map[constants.OPTION_A_KEY]["provider"]
|
374 |
+
provider_b: TTSProviderName = option_map[constants.OPTION_B_KEY]["provider"]
|
375 |
+
|
376 |
+
comparison_type: ComparisonType = _determine_comparison_type(provider_a, provider_b)
|
377 |
+
|
378 |
+
voting_results: VotingResults = {
|
379 |
+
"comparison_type": comparison_type,
|
380 |
+
"winning_provider": option_map[selected_option]["provider"],
|
381 |
+
"winning_option": selected_option,
|
382 |
+
"option_a_provider": provider_a,
|
383 |
+
"option_b_provider": provider_b,
|
384 |
+
"option_a_generation_id": option_map[constants.OPTION_A_KEY]["generation_id"],
|
385 |
+
"option_b_generation_id": option_map[constants.OPTION_B_KEY]["generation_id"],
|
386 |
+
"character_description": character_description,
|
387 |
+
"text": text,
|
388 |
+
"is_custom_text": text_modified,
|
389 |
+
}
|
390 |
+
|
391 |
+
await _persist_vote(db_session_maker, voting_results, config)
|
392 |
|
393 |
+
except Exception as e:
|
394 |
+
# Catch all exceptions at the top level of the background task
|
395 |
+
# to prevent unhandled exceptions in background tasks
|
396 |
+
logger.error(f"Background task error in submit_voting_results: {e}", exc_info=True)
|
397 |
|
398 |
|
399 |
def validate_env_var(var_name: str) -> str:
|
uv.lock
CHANGED
@@ -56,6 +56,38 @@ wheels = [
|
|
56 |
{ url = "https://files.pythonhosted.org/packages/46/eb/e7f063ad1fec6b3178a3cd82d1a3c4de82cccf283fc42746168188e1cdd5/anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a", size = 96041 },
|
57 |
]
|
58 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
59 |
[[package]]
|
60 |
name = "audioop-lts"
|
61 |
version = "0.2.1"
|
@@ -224,9 +256,11 @@ version = "0.1.0"
|
|
224 |
source = { virtual = "." }
|
225 |
dependencies = [
|
226 |
{ name = "anthropic" },
|
|
|
227 |
{ name = "elevenlabs" },
|
228 |
{ name = "gradio" },
|
229 |
-
{ name = "
|
|
|
230 |
{ name = "python-dotenv" },
|
231 |
{ name = "requests" },
|
232 |
{ name = "sqlalchemy" },
|
@@ -247,9 +281,11 @@ dev = [
|
|
247 |
[package.metadata]
|
248 |
requires-dist = [
|
249 |
{ name = "anthropic", specifier = ">=0.45.2" },
|
|
|
250 |
{ name = "elevenlabs", specifier = ">=1.50.7" },
|
251 |
{ name = "gradio", specifier = ">=5.15.0" },
|
252 |
-
{ name = "
|
|
|
253 |
{ name = "python-dotenv", specifier = ">=1.0.1" },
|
254 |
{ name = "requests", specifier = ">=2.32.3" },
|
255 |
{ name = "sqlalchemy", specifier = ">=2.0.0" },
|
@@ -872,19 +908,6 @@ wheels = [
|
|
872 |
{ url = "https://files.pythonhosted.org/packages/43/b3/df14c580d82b9627d173ceea305ba898dca135feb360b6d84019d0803d3b/pre_commit-4.1.0-py2.py3-none-any.whl", hash = "sha256:d29e7cb346295bcc1cc75fc3e92e343495e3ea0196c9ec6ba53f49f10ab6ae7b", size = 220560 },
|
873 |
]
|
874 |
|
875 |
-
[[package]]
|
876 |
-
name = "psycopg2"
|
877 |
-
version = "2.9.10"
|
878 |
-
source = { registry = "https://pypi.org/simple" }
|
879 |
-
sdist = { url = "https://files.pythonhosted.org/packages/62/51/2007ea29e605957a17ac6357115d0c1a1b60c8c984951c19419b3474cdfd/psycopg2-2.9.10.tar.gz", hash = "sha256:12ec0b40b0273f95296233e8750441339298e6a572f7039da5b260e3c8b60e11", size = 385672 }
|
880 |
-
wheels = [
|
881 |
-
{ url = "https://files.pythonhosted.org/packages/20/a2/c51ca3e667c34e7852157b665e3d49418e68182081060231d514dd823225/psycopg2-2.9.10-cp311-cp311-win32.whl", hash = "sha256:47c4f9875125344f4c2b870e41b6aad585901318068acd01de93f3677a6522c2", size = 1024538 },
|
882 |
-
{ url = "https://files.pythonhosted.org/packages/33/39/5a9a229bb5414abeb86e33b8fc8143ab0aecce5a7f698a53e31367d30caa/psycopg2-2.9.10-cp311-cp311-win_amd64.whl", hash = "sha256:0435034157049f6846e95103bd8f5a668788dd913a7c30162ca9503fdf542cb4", size = 1163736 },
|
883 |
-
{ url = "https://files.pythonhosted.org/packages/3d/16/4623fad6076448df21c1a870c93a9774ad8a7b4dd1660223b59082dd8fec/psycopg2-2.9.10-cp312-cp312-win32.whl", hash = "sha256:65a63d7ab0e067e2cdb3cf266de39663203d38d6a8ed97f5ca0cb315c73fe067", size = 1025113 },
|
884 |
-
{ url = "https://files.pythonhosted.org/packages/66/de/baed128ae0fc07460d9399d82e631ea31a1f171c0c4ae18f9808ac6759e3/psycopg2-2.9.10-cp312-cp312-win_amd64.whl", hash = "sha256:4a579d6243da40a7b3182e0430493dbd55950c493d8c68f4eec0b302f6bbf20e", size = 1163951 },
|
885 |
-
{ url = "https://files.pythonhosted.org/packages/ae/49/a6cfc94a9c483b1fa401fbcb23aca7892f60c7269c5ffa2ac408364f80dc/psycopg2-2.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:91fd603a2155da8d0cfcdbf8ab24a2d54bca72795b90d2a3ed2b6da8d979dee2", size = 2569060 },
|
886 |
-
]
|
887 |
-
|
888 |
[[package]]
|
889 |
name = "pydantic"
|
890 |
version = "2.10.6"
|
|
|
56 |
{ url = "https://files.pythonhosted.org/packages/46/eb/e7f063ad1fec6b3178a3cd82d1a3c4de82cccf283fc42746168188e1cdd5/anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a", size = 96041 },
|
57 |
]
|
58 |
|
59 |
+
[[package]]
|
60 |
+
name = "asyncpg"
|
61 |
+
version = "0.30.0"
|
62 |
+
source = { registry = "https://pypi.org/simple" }
|
63 |
+
sdist = { url = "https://files.pythonhosted.org/packages/2f/4c/7c991e080e106d854809030d8584e15b2e996e26f16aee6d757e387bc17d/asyncpg-0.30.0.tar.gz", hash = "sha256:c551e9928ab6707602f44811817f82ba3c446e018bfe1d3abecc8ba5f3eac851", size = 957746 }
|
64 |
+
wheels = [
|
65 |
+
{ url = "https://files.pythonhosted.org/packages/4c/0e/f5d708add0d0b97446c402db7e8dd4c4183c13edaabe8a8500b411e7b495/asyncpg-0.30.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5e0511ad3dec5f6b4f7a9e063591d407eee66b88c14e2ea636f187da1dcfff6a", size = 674506 },
|
66 |
+
{ url = "https://files.pythonhosted.org/packages/6a/a0/67ec9a75cb24a1d99f97b8437c8d56da40e6f6bd23b04e2f4ea5d5ad82ac/asyncpg-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:915aeb9f79316b43c3207363af12d0e6fd10776641a7de8a01212afd95bdf0ed", size = 645922 },
|
67 |
+
{ url = "https://files.pythonhosted.org/packages/5c/d9/a7584f24174bd86ff1053b14bb841f9e714380c672f61c906eb01d8ec433/asyncpg-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c198a00cce9506fcd0bf219a799f38ac7a237745e1d27f0e1f66d3707c84a5a", size = 3079565 },
|
68 |
+
{ url = "https://files.pythonhosted.org/packages/a0/d7/a4c0f9660e333114bdb04d1a9ac70db690dd4ae003f34f691139a5cbdae3/asyncpg-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3326e6d7381799e9735ca2ec9fd7be4d5fef5dcbc3cb555d8a463d8460607956", size = 3109962 },
|
69 |
+
{ url = "https://files.pythonhosted.org/packages/3c/21/199fd16b5a981b1575923cbb5d9cf916fdc936b377e0423099f209e7e73d/asyncpg-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:51da377487e249e35bd0859661f6ee2b81db11ad1f4fc036194bc9cb2ead5056", size = 3064791 },
|
70 |
+
{ url = "https://files.pythonhosted.org/packages/77/52/0004809b3427534a0c9139c08c87b515f1c77a8376a50ae29f001e53962f/asyncpg-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bc6d84136f9c4d24d358f3b02be4b6ba358abd09f80737d1ac7c444f36108454", size = 3188696 },
|
71 |
+
{ url = "https://files.pythonhosted.org/packages/52/cb/fbad941cd466117be58b774a3f1cc9ecc659af625f028b163b1e646a55fe/asyncpg-0.30.0-cp311-cp311-win32.whl", hash = "sha256:574156480df14f64c2d76450a3f3aaaf26105869cad3865041156b38459e935d", size = 567358 },
|
72 |
+
{ url = "https://files.pythonhosted.org/packages/3c/0a/0a32307cf166d50e1ad120d9b81a33a948a1a5463ebfa5a96cc5606c0863/asyncpg-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:3356637f0bd830407b5597317b3cb3571387ae52ddc3bca6233682be88bbbc1f", size = 629375 },
|
73 |
+
{ url = "https://files.pythonhosted.org/packages/4b/64/9d3e887bb7b01535fdbc45fbd5f0a8447539833b97ee69ecdbb7a79d0cb4/asyncpg-0.30.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c902a60b52e506d38d7e80e0dd5399f657220f24635fee368117b8b5fce1142e", size = 673162 },
|
74 |
+
{ url = "https://files.pythonhosted.org/packages/6e/eb/8b236663f06984f212a087b3e849731f917ab80f84450e943900e8ca4052/asyncpg-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aca1548e43bbb9f0f627a04666fedaca23db0a31a84136ad1f868cb15deb6e3a", size = 637025 },
|
75 |
+
{ url = "https://files.pythonhosted.org/packages/cc/57/2dc240bb263d58786cfaa60920779af6e8d32da63ab9ffc09f8312bd7a14/asyncpg-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c2a2ef565400234a633da0eafdce27e843836256d40705d83ab7ec42074efb3", size = 3496243 },
|
76 |
+
{ url = "https://files.pythonhosted.org/packages/f4/40/0ae9d061d278b10713ea9021ef6b703ec44698fe32178715a501ac696c6b/asyncpg-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1292b84ee06ac8a2ad8e51c7475aa309245874b61333d97411aab835c4a2f737", size = 3575059 },
|
77 |
+
{ url = "https://files.pythonhosted.org/packages/c3/75/d6b895a35a2c6506952247640178e5f768eeb28b2e20299b6a6f1d743ba0/asyncpg-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0f5712350388d0cd0615caec629ad53c81e506b1abaaf8d14c93f54b35e3595a", size = 3473596 },
|
78 |
+
{ url = "https://files.pythonhosted.org/packages/c8/e7/3693392d3e168ab0aebb2d361431375bd22ffc7b4a586a0fc060d519fae7/asyncpg-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:db9891e2d76e6f425746c5d2da01921e9a16b5a71a1c905b13f30e12a257c4af", size = 3641632 },
|
79 |
+
{ url = "https://files.pythonhosted.org/packages/32/ea/15670cea95745bba3f0352341db55f506a820b21c619ee66b7d12ea7867d/asyncpg-0.30.0-cp312-cp312-win32.whl", hash = "sha256:68d71a1be3d83d0570049cd1654a9bdfe506e794ecc98ad0873304a9f35e411e", size = 560186 },
|
80 |
+
{ url = "https://files.pythonhosted.org/packages/7e/6b/fe1fad5cee79ca5f5c27aed7bd95baee529c1bf8a387435c8ba4fe53d5c1/asyncpg-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:9a0292c6af5c500523949155ec17b7fe01a00ace33b68a476d6b5059f9630305", size = 621064 },
|
81 |
+
{ url = "https://files.pythonhosted.org/packages/3a/22/e20602e1218dc07692acf70d5b902be820168d6282e69ef0d3cb920dc36f/asyncpg-0.30.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05b185ebb8083c8568ea8a40e896d5f7af4b8554b64d7719c0eaa1eb5a5c3a70", size = 670373 },
|
82 |
+
{ url = "https://files.pythonhosted.org/packages/3d/b3/0cf269a9d647852a95c06eb00b815d0b95a4eb4b55aa2d6ba680971733b9/asyncpg-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c47806b1a8cbb0a0db896f4cd34d89942effe353a5035c62734ab13b9f938da3", size = 634745 },
|
83 |
+
{ url = "https://files.pythonhosted.org/packages/8e/6d/a4f31bf358ce8491d2a31bfe0d7bcf25269e80481e49de4d8616c4295a34/asyncpg-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b6fde867a74e8c76c71e2f64f80c64c0f3163e687f1763cfaf21633ec24ec33", size = 3512103 },
|
84 |
+
{ url = "https://files.pythonhosted.org/packages/96/19/139227a6e67f407b9c386cb594d9628c6c78c9024f26df87c912fabd4368/asyncpg-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46973045b567972128a27d40001124fbc821c87a6cade040cfcd4fa8a30bcdc4", size = 3592471 },
|
85 |
+
{ url = "https://files.pythonhosted.org/packages/67/e4/ab3ca38f628f53f0fd28d3ff20edff1c975dd1cb22482e0061916b4b9a74/asyncpg-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9110df111cabc2ed81aad2f35394a00cadf4f2e0635603db6ebbd0fc896f46a4", size = 3496253 },
|
86 |
+
{ url = "https://files.pythonhosted.org/packages/ef/5f/0bf65511d4eeac3a1f41c54034a492515a707c6edbc642174ae79034d3ba/asyncpg-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04ff0785ae7eed6cc138e73fc67b8e51d54ee7a3ce9b63666ce55a0bf095f7ba", size = 3662720 },
|
87 |
+
{ url = "https://files.pythonhosted.org/packages/e7/31/1513d5a6412b98052c3ed9158d783b1e09d0910f51fbe0e05f56cc370bc4/asyncpg-0.30.0-cp313-cp313-win32.whl", hash = "sha256:ae374585f51c2b444510cdf3595b97ece4f233fde739aa14b50e0d64e8a7a590", size = 560404 },
|
88 |
+
{ url = "https://files.pythonhosted.org/packages/c8/a4/cec76b3389c4c5ff66301cd100fe88c318563ec8a520e0b2e792b5b84972/asyncpg-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:f59b430b8e27557c3fb9869222559f7417ced18688375825f8f12302c34e915e", size = 621623 },
|
89 |
+
]
|
90 |
+
|
91 |
[[package]]
|
92 |
name = "audioop-lts"
|
93 |
version = "0.2.1"
|
|
|
256 |
source = { virtual = "." }
|
257 |
dependencies = [
|
258 |
{ name = "anthropic" },
|
259 |
+
{ name = "asyncpg" },
|
260 |
{ name = "elevenlabs" },
|
261 |
{ name = "gradio" },
|
262 |
+
{ name = "greenlet" },
|
263 |
+
{ name = "httpx" },
|
264 |
{ name = "python-dotenv" },
|
265 |
{ name = "requests" },
|
266 |
{ name = "sqlalchemy" },
|
|
|
281 |
[package.metadata]
|
282 |
requires-dist = [
|
283 |
{ name = "anthropic", specifier = ">=0.45.2" },
|
284 |
+
{ name = "asyncpg", specifier = ">=0.28.0" },
|
285 |
{ name = "elevenlabs", specifier = ">=1.50.7" },
|
286 |
{ name = "gradio", specifier = ">=5.15.0" },
|
287 |
+
{ name = "greenlet", specifier = ">=2.0.0" },
|
288 |
+
{ name = "httpx", specifier = ">=0.24.1" },
|
289 |
{ name = "python-dotenv", specifier = ">=1.0.1" },
|
290 |
{ name = "requests", specifier = ">=2.32.3" },
|
291 |
{ name = "sqlalchemy", specifier = ">=2.0.0" },
|
|
|
908 |
{ url = "https://files.pythonhosted.org/packages/43/b3/df14c580d82b9627d173ceea305ba898dca135feb360b6d84019d0803d3b/pre_commit-4.1.0-py2.py3-none-any.whl", hash = "sha256:d29e7cb346295bcc1cc75fc3e92e343495e3ea0196c9ec6ba53f49f10ab6ae7b", size = 220560 },
|
909 |
]
|
910 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
911 |
[[package]]
|
912 |
name = "pydantic"
|
913 |
version = "2.10.6"
|