This commit is contained in:
codey 2024-11-29 15:33:12 -05:00
parent 57f5564fe6
commit 6b62757dad
6 changed files with 230 additions and 35 deletions

View File

@ -0,0 +1,23 @@
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from typing import List
class ConnectionManager:
def __init__(self):
self.active_connections: List[WebSocket] = []
async def connect(self, websocket: WebSocket):
self.active_connections.append(websocket)
async def disconnect(self, websocket: WebSocket):
if websocket in self.active_connections:
self.active_connections.remove(websocket)
async def broadcast_raw(self, data: bytes):
for connection in self.active_connections:
try:
await connection.send_bytes(data)
except WebSocketDisconnect:
await self.disconnect(connection)
except Exception as e:
print(f"Error sending to client: {e}")

75
aces/flac_reader.py Normal file
View File

@ -0,0 +1,75 @@
#!/usr/bin/env python3.12
import soundfile as sf
import asyncio
import numpy as np
import time
from aces.connection_manager import ConnectionManager
from fastapi import WebSocket, WebSocketDisconnect
class AudioStreamer:
def __init__(self, sound_file, ws_connection_mgr):
self.sound_file = sound_file
self.audio_file = None
self.format_info = None
self.chunk_size = 16384 # Larger chunk for better quality
self.ws_connection_mgr = ws_connection_mgr
self.broadcast_task = None
self.init_audio_file()
def init_audio_file(self):
self.audio_file = sf.SoundFile(self.sound_file, 'rb')
self.format_info = {
'samplerate': self.audio_file.samplerate,
'channels': self.audio_file.channels,
'format': 'PCM',
'subtype': str(self.audio_file.subtype)
}
async def broadcast_audio(self):
try:
chunk_duration = self.chunk_size / (self.audio_file.samplerate * self.audio_file.channels)
target_interval = chunk_duration / 2 # Send chunks at twice playback rate for smooth buffering
while True:
if not self.ws_connection_mgr.active_connections:
await asyncio.sleep(0.1)
continue
start_time = asyncio.get_event_loop().time()
chunk = self.audio_file.read(self.chunk_size, dtype='float32')
if len(chunk) == 0:
self.audio_file.seek(0)
continue
await self.ws_connection_mgr.broadcast_raw(chunk.tobytes())
# Calculate how long processing took and adjust sleep time
elapsed = asyncio.get_event_loop().time() - start_time
sleep_time = max(0, target_interval - elapsed)
await asyncio.sleep(sleep_time)
except Exception as e:
print(f"Broadcast error: {e}")
async def handle_client(self, ws: WebSocket):
try:
await self.ws_connection_mgr.connect(ws)
await ws.send_json(self.format_info)
if not self.broadcast_task or self.broadcast_task.done():
self.broadcast_task = asyncio.create_task(self.broadcast_audio())
while True:
try:
await ws.receive_text()
except WebSocketDisconnect:
break
except Exception as e:
print(f"Error in handle_client: {e}")
finally:
await self.ws_connection_mgr.disconnect(ws)

View File

@ -33,6 +33,7 @@ glob_state = importlib.import_module("state").State(app, util, constants)
origins = [
"https://codey.lol",
"https://api.codey.lol"
]
app.add_middleware(CORSMiddleware,
@ -84,8 +85,6 @@ lastfm_endpoints = importlib.import_module("endpoints.lastfm").LastFM(app, util,
yt_endpoints = importlib.import_module("endpoints.yt").YT(app, util, constants, glob_state)
# Below: XC endpoint(s)
xc_endpoints = importlib.import_module("endpoints.xc").XC(app, util, constants, glob_state)
# Below: CAH endpoint(s)
# cah_endpoints = importlib.import_module("endpoints.cah").CAH(app, util, constants, glob_state)
# Below: Karma endpoint(s)
karma_endpoints = importlib.import_module("endpoints.karma").Karma(app, util, constants, glob_state)

View File

@ -40,6 +40,7 @@ class AI(FastAPI):
"""
/ai/base/
AI BASE Request
(Requires key)
"""
if not self.util.check_key(request.url.path, request.headers.get('X-Authd-With')):
@ -70,6 +71,7 @@ class AI(FastAPI):
"""
/ai/openai/
AI Request
(Requires key)
"""
if not self.util.check_key(request.url.path, request.headers.get('X-Authd-With')):
@ -145,9 +147,16 @@ class AI(FastAPI):
await self.glob_state.increment_counter('claude_ai_requests')
response = await request.json()
print(f"Response: {response}")
result = {
'resp': response.get('content')[0].get('text').strip()
}
if response.get('type') == 'error':
error_type = response.get('error').get('type')
error_message = response.get('error').get('message')
result = {
'resp': f"{error_type} error ({error_message})"
}
else:
result = {
'resp': response.get('content')[0].get('text').strip()
}
return result
except Exception as e: # pylint: disable=broad-exception-caught
logging.error("Error: %s", e)

View File

@ -3,6 +3,8 @@
import importlib
import urllib.parse
import regex
import aiohttp
import traceback
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
@ -14,6 +16,7 @@ class ValidLyricRequest(BaseModel):
- **s**: song
- **t**: track (artist and song combined) [used only if a & s are not used]
- **extra**: include extra details in response [optional, default: false]
- **lrc**: Request LRCs?
- **sub**: text to search within lyrics, if found lyrics will begin at found verse [optional]
- **src**: the script/utility which initiated the request
"""
@ -23,6 +26,7 @@ class ValidLyricRequest(BaseModel):
t: str | None = None
sub: str | None = None
extra: bool | None = False
lrc: bool | None = False
src: str
class Config: # pylint: disable=missing-class-docstring too-few-public-methods
@ -31,38 +35,12 @@ class ValidLyricRequest(BaseModel):
"a": "eminem",
"s": "rap god",
"src": "WEB",
"extra": True
"extra": True,
"lrc": False,
}
}
class ValidLyricRequest(BaseModel):
"""
- **a**: artist
- **s**: song
- **t**: track (artist and song combined) [used only if a & s are not used]
- **extra**: include extra details in response [optional, default: false]
- **sub**: text to search within lyrics, if found lyrics will begin at found verse [optional]
- **src**: the script/utility which initiated the request
"""
a: str | None = None
s: str | None = None
t: str | None = None
sub: str | None = None
extra: bool | None = False
src: str
class Config: # pylint: disable=missing-class-docstring too-few-public-methods
schema_extra = {
"example": {
"a": "eminem",
"s": "rap god",
"src": "WEB",
"extra": True
}
}
class ValidLyricSearchLogRequest(BaseModel):
"""
- **webradio**: whether or not to include requests generated automatically by the radio page on codey.lol, defaults to False
@ -99,6 +77,8 @@ class LyricSearch(FastAPI):
"IRC-SHARED"
]
self.lrc_regex = regex.compile(r'\[([0-9]{2}:[0-9]{2})\.[0-9]{1,3}\](\s(.*)){0,}')
for endpoint, handler in self.endpoints.items():
app.add_api_route(f"/{endpoint}/", handler, methods=["POST"])
@ -129,10 +109,12 @@ class LyricSearch(FastAPI):
- **s**: song
- **t**: track (artist and song combined) [used only if a & s are not used]
- **extra**: include extra details in response [optional, default: false]
- **lrc**: Request LRCs?
- **sub**: text to search within lyrics, if found lyrics will begin at found verse [optional, default: none]
- **src**: the script/utility which initiated the request
"""
lrc = data.lrc
src = data.src.upper()
if not src in self.acceptable_request_sources:
raise HTTPException(detail="Invalid request source", status_code=403)
@ -175,9 +157,89 @@ class LyricSearch(FastAPI):
else:
search_object = self.lyrics_engine.create_query_object(str(search_text))
if lrc:
search_worker = await self.lyrics_engine.grabFromSpotify(searching=search_object,
lrc=True)
spotify_lyrics_unsynced = True
if search_worker and search_worker.get('l'):
for line in search_worker.get('l'):
if line.get('timeTag') and line.get('timeTag') != "00:00.00":
spotify_lyrics_unsynced = False
if not search_worker or spotify_lyrics_unsynced:
# Try LRCLib before failing out
try:
lrclib_api_url = "https://lrclib.net/api/get"
sane_artist = urllib.parse.quote_plus(search_artist)
sane_track = urllib.parse.quote_plus(search_song)
async with aiohttp.ClientSession() as session:
async with session.get(f"{lrclib_api_url}?artist_name={sane_artist}&track_name={sane_track}") as request:
request.raise_for_status()
response_json = await request.json()
if not "syncedLyrics" in response_json:
raise BaseException("LRCLib Fallback Failed")
lrc_content = response_json.get('syncedLyrics')
returned_artist = response_json.get('artistName')
returned_song = response_json.get('trackName')
print(f"Synced Lyrics [LRCLib]: {lrc_content}")
lrc_content_out = []
for line in lrc_content.split("\n"):
_timetag = None
_words = None
if not line.strip():
continue
reg_helper = regex.findall(self.lrc_regex, line.strip())
if not reg_helper:
continue
reg_helper = reg_helper[0]
print(f"Reg helper: {reg_helper} for line: {line}; len: {len(reg_helper)}")
_timetag = reg_helper[0]
if not reg_helper[1].strip():
_words = ""
else:
_words = reg_helper[1]
lrc_content_out.append({
"timeTag": _timetag,
"words": _words,
})
return {
'err': False,
'artist': returned_artist,
'song': returned_song,
'combo_lev': "N/A",
'lrc': lrc_content_out,
'from_cache': False,
'src': 'Alt LRC SRC',
'reqn': await self.glob_state.get_counter('lyric_requests'),
}
except:
print(traceback.format_exc())
return {
'err': True,
'errorText': 'Search failed!',
}
return {
'err': True,
'errorText': 'Search failed!',
}
return {
'err': False,
'artist': search_worker['artist'],
'song': search_worker['song'],
'combo_lev': search_worker['combo_lev'],
'lrc': search_worker['l'],
'from_cache': False,
'src': search_worker['method'],
'reqn': await self.glob_state.get_counter('lyric_requests'),
}
search_worker = await self.lyrics_engine.lyrics_worker(searching=search_object,
recipient='anyone')
if not search_worker or not 'l' in search_worker.keys():
await self.glob_state.increment_counter('failedlyric_requests')
return {

View File

@ -1,8 +1,21 @@
#!/usr/bin/env python3.12
from fastapi import FastAPI, Request, HTTPException
from fastapi import FastAPI, Request, HTTPException, WebSocket, WebSocketDisconnect, WebSocketException
from pydantic import BaseModel
from aiohttp import ClientSession, ClientTimeout
from aces.connection_manager import ConnectionManager
from aces.flac_reader import AudioStreamer
import os
import asyncio
import pyaudio
import wave
import traceback
import pyflac.decoder as decoder
import numpy as np
import soundfile as sf
import json
import time
class ValidXCRequest(BaseModel):
"""
@ -17,6 +30,8 @@ class ValidXCRequest(BaseModel):
cmd: str
data: dict | None = None
class XC(FastAPI):
"""XC (CrossComm) Endpoints"""
def __init__(self, app: FastAPI, util, constants, glob_state): # pylint: disable=super-init-not-called
@ -25,14 +40,26 @@ class XC(FastAPI):
self.constants = constants
self.glob_state = glob_state
self.ws_endpoints = {
# "aces_ws_put": self.put_ws_handler,
}
self.endpoints = {
"xc": self.xc_handler,
#tbd
}
for endpoint, handler in self.ws_endpoints.items():
app.add_api_websocket_route(f"/{endpoint}/", handler)
for endpoint, handler in self.endpoints.items():
app.add_api_route(f"/{endpoint}/", handler, methods=["POST"])
# async def put_ws_handler(self, ws: WebSocket):
# await ws.accept()
# await self.audio_streamer.handle_client(ws)
async def xc_handler(self, data: ValidXCRequest, request: Request):
"""
/xc/