Add few changes
This commit is contained in:
95
README.md
95
README.md
@@ -28,7 +28,7 @@ Note that run is also the default recipe. So `make` works too.
|
||||
|
||||
The run recipe should take care of everything for you... enjoy!
|
||||
|
||||
## Running translation server
|
||||
### Running translation server
|
||||
|
||||
To start translation server for subtitle translation, run:
|
||||
|
||||
@@ -37,13 +37,13 @@ To start translation server for subtitle translation, run:
|
||||
You can keep it running through multiple `make run`'s.
|
||||
The makefile will pre-install everything if needed.
|
||||
|
||||
## Cleaning up the mess
|
||||
### Cleaning up the mess
|
||||
|
||||
If you wish to go back to a clean slate just run:
|
||||
|
||||
`make clean`
|
||||
|
||||
## Packing for production
|
||||
### Packing for production
|
||||
|
||||
To make an archive `pythagoras.tar.xz` in project root simply run:
|
||||
|
||||
@@ -51,13 +51,21 @@ To make an archive `pythagoras.tar.xz` in project root simply run:
|
||||
|
||||
## Usage
|
||||
|
||||
To connect to the WebSocket for receiving subtitles and messages, simply use the `/ws` endpoint, for example like this:
|
||||
To connect to the WebSocket for receiving subtitles, messages and commands, simply use the `/ws` endpoint, for example like this:
|
||||
|
||||
`
|
||||
websocat ws://localhost:8000/ws
|
||||
`
|
||||
|
||||
To push new subtitles onto the server, use the `/subtitles` endpoints. There are currently two different ones, and each produces different response type. Here are some examples:
|
||||
### Subtitles
|
||||
|
||||
Here is a list of all subtitle types:
|
||||
|
||||
- `subtitle_en_submit_sentence`, `subtitle_cs_submit_sentence`, `subtitle_en_update_current`, `subtitle_cs_update_current`
|
||||
|
||||
To push new subtitles onto the server, use the `/subtitles/` endpoints. There are currently two different ones, and each produces different response type. When translation is enabled, each sentence submission yields two websocket messages, one in English and one in Czech. Here are some examples:
|
||||
|
||||
___
|
||||
|
||||
- The first is `/subtitles/update_current`, which serves the purpose of updating the current sentence as it comes. Here is an example:
|
||||
|
||||
@@ -65,29 +73,50 @@ To push new subtitles onto the server, use the `/subtitles` endpoints. There are
|
||||
curl -X POST http://localhost:8000/subtitles/update_current -H "Content-Type: text/plain" -d 'I love pushing '
|
||||
`
|
||||
|
||||
___
|
||||
|
||||
- The second is `/subtitles/submit_sentence`, which is used to submit the next finished sentence. Here is an example:
|
||||
|
||||
`
|
||||
curl -X POST http://localhost:8000/subtitles/submit_sentence -H "Content-Type: text/plain" -d 'I love pushing sentences to servers!'
|
||||
`
|
||||
|
||||
___
|
||||
|
||||
|
||||
### Commands
|
||||
|
||||
Here is a list of all commands:
|
||||
|
||||
- `getselectedmessage`, `setlibretranslate`, `setautopolling`, `autopollingrate`, `playvideo`, `seekvideo`, `setscreen_main`, `setscreen_video`, `setscreen_idle`
|
||||
|
||||
To control the server using commands, use the `/control` endpoint, for example like this:
|
||||
|
||||
___
|
||||
|
||||
- Poll the peehaitchpea server for the latest message to display:
|
||||
|
||||
`
|
||||
curl -X POST http://localhost:8000/control -H "Content-Type: application/json" -d '{"command": "getselectedmessage"}'
|
||||
`
|
||||
|
||||
___
|
||||
|
||||
- Change the state of the LibreTranslate translation module (active or deactivated):
|
||||
|
||||
`
|
||||
curl -X POST http://localhost:8000/control -H "Content-Type: application/json" -d '{"command": "setlibretranslate", "state": false}'
|
||||
`
|
||||
|
||||
___
|
||||
|
||||
- Change the state of auto-polling:
|
||||
|
||||
`
|
||||
curl -X POST http://localhost:8000/control -H "Content-Type: application/json" -d '{"command": "setautopolling", "state": false}'
|
||||
`
|
||||
|
||||
`
|
||||
curl -X POST http://localhost:8000/control -H "Content-Type: application/json" -d '{"command": "setautopolling", "state": true}'
|
||||
`
|
||||
___
|
||||
|
||||
- Set the polling rate to a value (in seconds):
|
||||
|
||||
@@ -95,6 +124,52 @@ curl -X POST http://localhost:8000/control -H "Content-Type: application/json" -
|
||||
curl -X POST http://localhost:8000/control -H "Content-Type: application/json" -d '{"command": "autopollingrate", "rate": 10}'
|
||||
`
|
||||
|
||||
## TODO
|
||||
___
|
||||
|
||||
- Create a command for playing a video (the TED talk RMS likes to play at the beginning of his lecture)
|
||||
- Tell the client to play a video, defined by the filename parameter:
|
||||
|
||||
`
|
||||
curl -X POST http://localhost:8000/control -H "Content-Type: application/json" -d '{"command": "playvideo", "filename": "stallman.webm}'
|
||||
`
|
||||
|
||||
___
|
||||
|
||||
- Seek the currently played video to a timestamp (in seconds):
|
||||
|
||||
`
|
||||
curl -X POST http://localhost:8000/control -H "Content-Type: application/json" -d '{"command": "seekvideo", "timestamp": 420}'
|
||||
`
|
||||
|
||||
___
|
||||
|
||||
- Set the screen of the client to the main view (subtitles and messages):
|
||||
|
||||
`
|
||||
curl -X POST http://localhost:8000/control -H "Content-Type: application/json" -d '{"command": "setscreen_main"}'
|
||||
`
|
||||
|
||||
___
|
||||
|
||||
- Set the screen of the client to the video playback:
|
||||
|
||||
`
|
||||
curl -X POST http://localhost:8000/control -H "Content-Type: application/json" -d '{"command": "setscreen_video"}'
|
||||
`
|
||||
|
||||
___
|
||||
|
||||
- Set the screen of the client to the idle screen (this is the default screen after startup of the app):
|
||||
|
||||
`
|
||||
curl -X POST http://localhost:8000/control -H "Content-Type: application/json" -d '{"command": "setscreen_idle"}'
|
||||
`
|
||||
|
||||
___
|
||||
|
||||
### Media
|
||||
|
||||
You can also request media from Pythagoras using the `/media/` endpoint, which will yield you the standard HTTP FileResponse, here is an example:
|
||||
|
||||
`
|
||||
curl -X GET http://localhost:8000/media/stallman.webm
|
||||
`
|
||||
|
81
main.py
81
main.py
@@ -27,6 +27,8 @@ app = FastAPI(title="Pythagoras", description="A proxy service handling HTTP and
|
||||
app.state.auto_polling = False
|
||||
app.state.polling_rate = 5
|
||||
app.state.enable_libretranslate = True
|
||||
app.state.client_state = "idle"
|
||||
app.state.latest_message = ""
|
||||
|
||||
# Define the media directory
|
||||
MEDIA_DIR = Path("./media")
|
||||
@@ -43,12 +45,24 @@ class ConnectionManager:
|
||||
async def connect(self, websocket: WebSocket):
|
||||
await websocket.accept()
|
||||
self.active_connections.append(websocket)
|
||||
await setscreen_single(websocket, app.state.client_state) # Send the latest client state to any new client
|
||||
await self.singlecast_binary({"type": "selectedmessage", "message": app.state.latest_message}, websocket) # Broadcast latest message to all clients
|
||||
logger.info(f"WebSocket client connected. Total connections: {len(self.active_connections)}")
|
||||
|
||||
def disconnect(self, websocket: WebSocket):
|
||||
self.active_connections.remove(websocket)
|
||||
logger.info(f"WebSocket client disconnected. Total connections: {len(self.active_connections)}")
|
||||
|
||||
def json_to_binary(self, json_str: str):
|
||||
json_bytes = json_str.encode('utf-8')
|
||||
json_length = len(json_bytes)
|
||||
|
||||
# 4-byte unsigned integer (uint32)
|
||||
length_bytes = struct.pack('!I', json_length)
|
||||
|
||||
return length_bytes + json_bytes
|
||||
|
||||
|
||||
async def broadcast(self, message: str):
|
||||
for connection in self.active_connections:
|
||||
await connection.send_text(message)
|
||||
@@ -61,21 +75,29 @@ class ConnectionManager:
|
||||
return
|
||||
|
||||
json_str = json.dumps(data_dict)
|
||||
json_bytes = json_str.encode('utf-8')
|
||||
json_length = len(json_bytes)
|
||||
|
||||
# 4-byte unsigned integer (uint32)
|
||||
length_bytes = struct.pack('!I', json_length)
|
||||
|
||||
message_bytes = length_bytes + json_bytes
|
||||
message_bytes = self.json_to_binary(json_str)
|
||||
|
||||
for connection in self.active_connections:
|
||||
try:
|
||||
await connection.send_bytes(message_bytes)
|
||||
logger.debug(f"Sent binary message ({json_length} bytes) to a client")
|
||||
logger.debug(f"Sent binary message ({len(message_bytes)} bytes) to a client")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send binary message: {str(e)}")
|
||||
|
||||
async def singlecast_binary(self, data_dict: dict, ws: WebSocket):
|
||||
"""
|
||||
I love code duplication
|
||||
"""
|
||||
|
||||
json_str = json.dumps(data_dict)
|
||||
message_bytes = self.json_to_binary(json_str)
|
||||
|
||||
try:
|
||||
await ws.send_bytes(message_bytes)
|
||||
logger.debug(f"Sent binary message ({len(message_bytes)} bytes) to a client")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send binary message: {str(e)}")
|
||||
|
||||
manager = ConnectionManager()
|
||||
|
||||
# Static files
|
||||
@@ -126,6 +148,33 @@ async def control_endpoint(request: Request):
|
||||
app.state.polling_rate = new_rate
|
||||
logger.info(f"Auto-polling rate change requested: {new_rate} seconds")
|
||||
|
||||
elif data['command'] == "playvideo" and 'filename' in data:
|
||||
filename = data['filename']
|
||||
await playvideo(filename)
|
||||
logger.info(f"Video playback requested: {filename}")
|
||||
|
||||
elif data['command'] == "seekvideo" and 'timestamp' in data:
|
||||
timestamp = data['timestamp']
|
||||
await seekvideo(timestamp)
|
||||
logger.info(f"Seeking of the currently playing video requested to {timestamp} seconds")
|
||||
|
||||
|
||||
|
||||
elif data['command'] == "setscreen_main":
|
||||
await setscreen("main")
|
||||
app.state.client_state = "main"
|
||||
logger.info(f"Setting the client screen to main view.")
|
||||
|
||||
elif data['command'] == "setscreen_video":
|
||||
await setscreen("video")
|
||||
app.state.client_state = "video"
|
||||
logger.info(f"Setting the client screen to video playback.")
|
||||
|
||||
elif data['command'] == "setscreen_idle":
|
||||
await setscreen("idle")
|
||||
app.state.client_state = "idle"
|
||||
logger.info(f"Setting the client screen to idle.")
|
||||
|
||||
|
||||
return JSONResponse(
|
||||
status_code=200,
|
||||
@@ -246,6 +295,19 @@ async def translate_to_cs_libre(text: str):
|
||||
logger.error(f"Translation error: {str(e)}")
|
||||
return text
|
||||
|
||||
async def setscreen_single(ws: WebSocket, screen: str):
|
||||
return await manager.singlecast_binary({"type": "setscreen", "screen": screen}, ws)
|
||||
|
||||
async def setscreen(screen: str):
|
||||
return await manager.broadcast_binary({"type": "setscreen", "screen": screen})
|
||||
|
||||
async def playvideo(filename: str):
|
||||
return await manager.broadcast_binary({"type": "playvideo", "filename": filename})
|
||||
|
||||
async def seekvideo(timestamp: int):
|
||||
return await manager.broadcast_binary({"type": "seekvideo", "timestamp": timestamp})
|
||||
|
||||
|
||||
|
||||
|
||||
async def fetch_selected_message():
|
||||
@@ -272,9 +334,10 @@ async def fetch_selected_message():
|
||||
|
||||
async def process_selected_message(message: str):
|
||||
"""
|
||||
Processes the selected message.
|
||||
Processes the selected message and saves it to cache.
|
||||
"""
|
||||
logger.info(f"Processing message: {message}")
|
||||
app.state.latest_message = message
|
||||
if manager.active_connections:
|
||||
await manager.broadcast_binary({"type": "selectedmessage", "message": message})
|
||||
|
||||
|
Reference in New Issue
Block a user