restructure the project for packaging
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
|||||||
/venv/
|
/venv/
|
||||||
|
*.egg-info
|
||||||
*.pyc
|
*.pyc
|
||||||
19
Dockerfile
Normal file
19
Dockerfile
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
FROM python:3.12-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy only necessary files and directories
|
||||||
|
COPY src/auracaster_webui/ ./src/auracaster_webui/
|
||||||
|
COPY src/api_client/ ./src/api_client/
|
||||||
|
|
||||||
|
# Install the package
|
||||||
|
RUN pip install --no-cache-dir .
|
||||||
|
|
||||||
|
# Expose Streamlit port
|
||||||
|
EXPOSE 8501
|
||||||
|
|
||||||
|
# Set environment variables
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
# Run the Streamlit app
|
||||||
|
CMD ["auracaster-webui"]
|
||||||
62
README.md
Normal file
62
README.md
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
# Auracaster - Airport Announcement System
|
||||||
|
|
||||||
|
A modern web application for managing and broadcasting announcements throughout an airport. This system allows for quick and efficient communication with passengers in multiple areas and languages.
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
The project is organized into three main packages:
|
||||||
|
|
||||||
|
- **auracaster_webui**: The Streamlit frontend application for the announcement system.
|
||||||
|
- **api_client**: Client library for communicating with the backend API.
|
||||||
|
- **mock_backend**: A mock implementation of the backend API for development and testing.
|
||||||
|
|
||||||
|
## Setup and Installation
|
||||||
|
|
||||||
|
### Development Setup
|
||||||
|
|
||||||
|
1. Create a virtual environment:
|
||||||
|
```
|
||||||
|
python -m venv venv
|
||||||
|
source venv/bin/activate # On Windows: venv\Scripts\activate
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Install the package in development mode:
|
||||||
|
```
|
||||||
|
pip install -e .
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Run the mock system (includes both frontend and backend):
|
||||||
|
```
|
||||||
|
auracaster-mock
|
||||||
|
```
|
||||||
|
|
||||||
|
4. For frontend only (requires a running backend):
|
||||||
|
```
|
||||||
|
auracaster-webui
|
||||||
|
```
|
||||||
|
|
||||||
|
## Docker
|
||||||
|
|
||||||
|
The project includes a Dockerfile for building a container that packages the webui and api_client components (without the mock backend).
|
||||||
|
|
||||||
|
Build the Docker image:
|
||||||
|
```
|
||||||
|
docker build -t auracaster:latest .
|
||||||
|
```
|
||||||
|
|
||||||
|
Run the container:
|
||||||
|
```
|
||||||
|
docker run -p 8501:8501 -e API_BASE_URL=http://backend-host:7999 auracaster:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
- `API_BASE_URL`: URL of the backend API server (default: http://localhost:7999)
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Create, update, and delete endpoint groups
|
||||||
|
- Make announcements to specific areas of the airport
|
||||||
|
- Monitor announcement status in real-time
|
||||||
|
- Support for multiple languages
|
||||||
|
- Predefined announcement templates for common scenarios
|
||||||
62
main_mock.py
62
main_mock.py
@@ -1,6 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
Main entry point for the Airport Announcement System.
|
Main entry point for the Airport Announcement System mock backend.
|
||||||
This script starts both the backend API and the Streamlit frontend.
|
|
||||||
"""
|
"""
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
@@ -21,27 +20,44 @@ def stream_output(process, prefix):
|
|||||||
def start_backend():
|
def start_backend():
|
||||||
"""Start the backend API server."""
|
"""Start the backend API server."""
|
||||||
print("Starting backend API server...")
|
print("Starting backend API server...")
|
||||||
backend_process = subprocess.Popen(
|
try:
|
||||||
[sys.executable, "-m", "uvicorn", "mock_backend.mock_api:app", "--host", "0.0.0.0", "--port", "7999", "--reload"],
|
# Add verbose output for debugging
|
||||||
stdout=subprocess.PIPE,
|
print("Current working directory:", os.getcwd())
|
||||||
stderr=subprocess.PIPE,
|
print("Python path:", sys.path)
|
||||||
text=True,
|
|
||||||
bufsize=1 # Line buffered
|
backend_process = subprocess.Popen(
|
||||||
)
|
[sys.executable, "-m", "uvicorn", "mock_backend.mock_api:app", "--host", "0.0.0.0", "--port", "7999", "--reload", "--log-level", "debug"],
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
# Start a thread to stream the backend output
|
stderr=subprocess.PIPE,
|
||||||
backend_thread = threading.Thread(target=stream_output, args=(backend_process, "BACKEND"), daemon=True)
|
text=True,
|
||||||
backend_thread.start()
|
bufsize=1 # Line buffered
|
||||||
|
)
|
||||||
# Wait a moment to ensure the backend has started
|
|
||||||
time.sleep(2)
|
# Start a thread to stream the backend output
|
||||||
return backend_process
|
backend_thread = threading.Thread(target=stream_output, args=(backend_process, "BACKEND"), daemon=True)
|
||||||
|
backend_thread.start()
|
||||||
|
|
||||||
|
# Wait a moment to ensure the backend has started
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
# Try to ping the API to make sure it's up
|
||||||
|
try:
|
||||||
|
import requests
|
||||||
|
response = requests.get("http://localhost:7999/groups", timeout=1)
|
||||||
|
print(f"Backend API check: status code {response.status_code}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Warning: Backend API check failed: {str(e)}")
|
||||||
|
|
||||||
|
return backend_process
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error starting backend: {str(e)}")
|
||||||
|
raise
|
||||||
|
|
||||||
def start_frontend():
|
def start_frontend():
|
||||||
"""Start the Streamlit frontend."""
|
"""Start the Streamlit frontend."""
|
||||||
print("Starting Streamlit frontend...")
|
print("Starting Streamlit frontend...")
|
||||||
frontend_process = subprocess.Popen(
|
frontend_process = subprocess.Popen(
|
||||||
[sys.executable, "-m", "streamlit", "run", "auracaster-webui.py"],
|
[sys.executable, "-m", "streamlit", "run", "./src/auracaster_webui/app.py"],
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
stderr=subprocess.PIPE,
|
stderr=subprocess.PIPE,
|
||||||
text=True,
|
text=True,
|
||||||
@@ -54,10 +70,8 @@ def start_frontend():
|
|||||||
|
|
||||||
return frontend_process
|
return frontend_process
|
||||||
|
|
||||||
if __name__ == "__main__":
|
def run_mock():
|
||||||
# Ensure we're in the correct directory
|
"""Run the mock backend and frontend."""
|
||||||
os.chdir(Path(__file__).parent)
|
|
||||||
|
|
||||||
# Start the backend and frontend
|
# Start the backend and frontend
|
||||||
backend_process = start_backend()
|
backend_process = start_backend()
|
||||||
frontend_process = start_frontend()
|
frontend_process = start_frontend()
|
||||||
@@ -75,3 +89,7 @@ if __name__ == "__main__":
|
|||||||
backend_process.terminate()
|
backend_process.terminate()
|
||||||
frontend_process.terminate()
|
frontend_process.terminate()
|
||||||
print("Shutdown complete.")
|
print("Shutdown complete.")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
os.chdir(os.path.dirname(__file__))
|
||||||
|
run_mock()
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
"""
|
|
||||||
Mock backend for the Airport Announcement System.
|
|
||||||
"""
|
|
||||||
@@ -1,207 +0,0 @@
|
|||||||
import time
|
|
||||||
import threading
|
|
||||||
from typing import Optional, List
|
|
||||||
|
|
||||||
from api_models import EndpointGroup, AnnouncementStates
|
|
||||||
|
|
||||||
AVAILABLE_ENDPOINTS = [f"endpoint{i}" for i in range(1, 4)] # Predefined endpoints
|
|
||||||
AVAILABLE_LANGUAGES = ["German", "English", "French", "Spanish", "Italian"]
|
|
||||||
|
|
||||||
class AnnouncementSystem:
|
|
||||||
def __init__(self):
|
|
||||||
self.available_endpoints = AVAILABLE_ENDPOINTS
|
|
||||||
self.available_languages = AVAILABLE_LANGUAGES
|
|
||||||
self.endpoint_groups = [
|
|
||||||
EndpointGroup(
|
|
||||||
id=1,
|
|
||||||
name="Gate1",
|
|
||||||
endpoints=["endpoint1", "endpoint2"],
|
|
||||||
languages=["German", "English"]
|
|
||||||
),
|
|
||||||
EndpointGroup(
|
|
||||||
id=2,
|
|
||||||
name="Gate2",
|
|
||||||
endpoints=["endpoint3"],
|
|
||||||
languages=["German", "English"]
|
|
||||||
)
|
|
||||||
]
|
|
||||||
self.active_group_id = None
|
|
||||||
self._thread = None
|
|
||||||
self.last_completed_group_id = None
|
|
||||||
|
|
||||||
def start_announcement(self, text: str, group: EndpointGroup):
|
|
||||||
# Find the group in our list to ensure we're working with the actual instance
|
|
||||||
target_group = self.get_endpoint_group(group.id)
|
|
||||||
if not target_group:
|
|
||||||
raise ValueError(f"Group with ID {group.id} not found")
|
|
||||||
|
|
||||||
# Check if this group has an announcement in progress
|
|
||||||
if target_group.progress.current_state not in [AnnouncementStates.IDLE.value, AnnouncementStates.COMPLETE.value]:
|
|
||||||
raise Exception("Announcement already in progress for this group")
|
|
||||||
|
|
||||||
# Check if any other announcement is in progress
|
|
||||||
for g in self.endpoint_groups:
|
|
||||||
if g.progress.current_state not in [AnnouncementStates.IDLE.value, AnnouncementStates.COMPLETE.value]:
|
|
||||||
raise Exception("Another announcement is already in progress")
|
|
||||||
|
|
||||||
# If this group was the last completed group, clear that reference
|
|
||||||
if self.last_completed_group_id == target_group.id:
|
|
||||||
self.last_completed_group_id = None
|
|
||||||
|
|
||||||
# Reset the group to IDLE state if it was in COMPLETE state
|
|
||||||
if target_group.progress.current_state == AnnouncementStates.COMPLETE.value:
|
|
||||||
target_group.progress.current_state = AnnouncementStates.IDLE.value
|
|
||||||
target_group.progress.progress = 0.0
|
|
||||||
|
|
||||||
# Set up the announcement parameters
|
|
||||||
target_group.parameters.text = text
|
|
||||||
target_group.parameters.languages = target_group.languages.copy()
|
|
||||||
target_group.parameters.start_time = time.time()
|
|
||||||
self.active_group_id = target_group.id
|
|
||||||
|
|
||||||
# Start the announcement process in a separate thread
|
|
||||||
self._thread = threading.Thread(target=self._run_process, args=(target_group,))
|
|
||||||
self._thread.start()
|
|
||||||
|
|
||||||
def _run_process(self, group: EndpointGroup):
|
|
||||||
try:
|
|
||||||
self._update_state(group, AnnouncementStates.TRANSLATING)
|
|
||||||
time.sleep(1) # Simulate translation
|
|
||||||
|
|
||||||
self._update_state(group, AnnouncementStates.GENERATING_VOICE)
|
|
||||||
time.sleep(1) # Voice synthesis
|
|
||||||
|
|
||||||
self._update_state(group, AnnouncementStates.ROUTING)
|
|
||||||
time.sleep(len(group.endpoints) * 0.5)
|
|
||||||
|
|
||||||
self._update_state(group, AnnouncementStates.ACTIVE)
|
|
||||||
time.sleep(1) # Simulate broadcast
|
|
||||||
|
|
||||||
self._update_state(group, AnnouncementStates.COMPLETE)
|
|
||||||
self.last_completed_group_id = group.id
|
|
||||||
self.active_group_id = None
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
group.progress.error = str(e)
|
|
||||||
self._update_state(group, AnnouncementStates.ERROR)
|
|
||||||
self.active_group_id = None
|
|
||||||
|
|
||||||
def _update_state(self, group: EndpointGroup, new_state: AnnouncementStates):
|
|
||||||
group.progress.current_state = new_state.value
|
|
||||||
|
|
||||||
# Progress based on state transitions
|
|
||||||
state_progress = {
|
|
||||||
AnnouncementStates.TRANSLATING: 0,
|
|
||||||
AnnouncementStates.GENERATING_VOICE: 0.25,
|
|
||||||
AnnouncementStates.ROUTING: 0.5,
|
|
||||||
AnnouncementStates.ACTIVE: 0.75,
|
|
||||||
AnnouncementStates.COMPLETE: 1.0,
|
|
||||||
AnnouncementStates.ERROR: 0
|
|
||||||
}
|
|
||||||
group.progress.progress = state_progress[new_state]
|
|
||||||
|
|
||||||
def get_endpoint_groups(self) -> List[EndpointGroup]:
|
|
||||||
return self.endpoint_groups
|
|
||||||
|
|
||||||
def get_endpoint_group(self, group_id: int) -> Optional[EndpointGroup]:
|
|
||||||
return next((g for g in self.endpoint_groups if g.id == group_id), None)
|
|
||||||
|
|
||||||
def add_endpoint_group(self, group: EndpointGroup) -> EndpointGroup:
|
|
||||||
if any(g.id == group.id for g in self.endpoint_groups):
|
|
||||||
raise ValueError(f"Group with ID {group.id} already exists")
|
|
||||||
self.endpoint_groups.append(group)
|
|
||||||
return group
|
|
||||||
|
|
||||||
def update_endpoint_group(self, group_id: int, updated_group: EndpointGroup) -> EndpointGroup:
|
|
||||||
if group_id != updated_group.id:
|
|
||||||
raise ValueError("Group ID cannot be changed")
|
|
||||||
|
|
||||||
group = self.get_endpoint_group(group_id)
|
|
||||||
if not group:
|
|
||||||
raise ValueError(f"Group with ID {group_id} not found")
|
|
||||||
|
|
||||||
# Update only the editable fields, not the state fields
|
|
||||||
group.name = updated_group.name
|
|
||||||
group.endpoints = updated_group.endpoints
|
|
||||||
group.languages = updated_group.languages
|
|
||||||
return group
|
|
||||||
|
|
||||||
def delete_endpoint_group(self, group_id: int) -> None:
|
|
||||||
group = self.get_endpoint_group(group_id)
|
|
||||||
if not group:
|
|
||||||
raise ValueError(f"Group with ID {group_id} not found")
|
|
||||||
|
|
||||||
# Cannot delete a group with an active announcement
|
|
||||||
if group.progress.current_state not in [AnnouncementStates.IDLE.value, AnnouncementStates.COMPLETE.value]:
|
|
||||||
raise ValueError(f"Cannot delete group with an active announcement")
|
|
||||||
|
|
||||||
self.endpoint_groups = [g for g in self.endpoint_groups if g.id != group_id]
|
|
||||||
|
|
||||||
def get_available_languages(self) -> List[str]:
|
|
||||||
"""Get the list of available languages for announcements."""
|
|
||||||
return self.available_languages
|
|
||||||
|
|
||||||
def get_available_endpoints(self) -> List[str]:
|
|
||||||
"""Get the list of available endpoints for announcements."""
|
|
||||||
return self.available_endpoints
|
|
||||||
|
|
||||||
def get_announcement_status(self) -> dict:
|
|
||||||
"""Get the status of the current announcement."""
|
|
||||||
if self.active_group_id is not None:
|
|
||||||
active_group = self.get_endpoint_group(self.active_group_id)
|
|
||||||
return {
|
|
||||||
"state": active_group.progress.current_state,
|
|
||||||
"progress": active_group.progress.progress,
|
|
||||||
"error": active_group.progress.error,
|
|
||||||
"details": {
|
|
||||||
"text": active_group.parameters.text,
|
|
||||||
"languages": active_group.parameters.languages or active_group.languages,
|
|
||||||
"group": {
|
|
||||||
"id": active_group.id,
|
|
||||||
"name": active_group.name,
|
|
||||||
"endpoints": active_group.endpoints,
|
|
||||||
"languages": active_group.languages
|
|
||||||
},
|
|
||||||
"start_time": active_group.parameters.start_time
|
|
||||||
}
|
|
||||||
}
|
|
||||||
elif self.last_completed_group_id is not None:
|
|
||||||
# Return the last completed announcement
|
|
||||||
completed_group = self.get_endpoint_group(self.last_completed_group_id)
|
|
||||||
return {
|
|
||||||
"state": completed_group.progress.current_state,
|
|
||||||
"progress": completed_group.progress.progress,
|
|
||||||
"error": completed_group.progress.error,
|
|
||||||
"details": {
|
|
||||||
"text": completed_group.parameters.text,
|
|
||||||
"languages": completed_group.parameters.languages or completed_group.languages,
|
|
||||||
"group": {
|
|
||||||
"id": completed_group.id,
|
|
||||||
"name": completed_group.name,
|
|
||||||
"endpoints": completed_group.endpoints,
|
|
||||||
"languages": completed_group.languages
|
|
||||||
},
|
|
||||||
"start_time": completed_group.parameters.start_time
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
# No active or completed announcements
|
|
||||||
return {
|
|
||||||
"state": AnnouncementStates.IDLE.value,
|
|
||||||
"progress": 0.0,
|
|
||||||
"error": None,
|
|
||||||
"details": {
|
|
||||||
"text": "",
|
|
||||||
"languages": [],
|
|
||||||
"group": {
|
|
||||||
"id": 0,
|
|
||||||
"name": "",
|
|
||||||
"endpoints": [],
|
|
||||||
"languages": []
|
|
||||||
},
|
|
||||||
"start_time": 0.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Singleton instance
|
|
||||||
announcement_system = AnnouncementSystem()
|
|
||||||
40
pyproject.toml
Normal file
40
pyproject.toml
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=61.0", "wheel"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "auracaster"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Airport Announcement System"
|
||||||
|
readme = "README.md"
|
||||||
|
authors = [
|
||||||
|
{name = "Airport Team"}
|
||||||
|
]
|
||||||
|
requires-python = ">=3.8"
|
||||||
|
classifiers = [
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"License :: OSI Approved :: MIT License",
|
||||||
|
"Operating System :: OS Independent",
|
||||||
|
]
|
||||||
|
dependencies = [
|
||||||
|
"streamlit>=1.25.0",
|
||||||
|
"requests>=2.28.0",
|
||||||
|
"fastapi>=0.95.0",
|
||||||
|
"uvicorn>=0.22.0",
|
||||||
|
"pydantic>=1.10.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
dev = [
|
||||||
|
"pytest>=7.0.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.setuptools]
|
||||||
|
package-dir = {"" = "src"}
|
||||||
|
|
||||||
|
[tool.setuptools.packages.find]
|
||||||
|
where = ["src"]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
auracaster-webui = "auracaster_webui.main:run_app"
|
||||||
|
auracaster-mock = "mock_backend.main:run_mock"
|
||||||
0
src/api_client/__init__.py
Normal file
0
src/api_client/__init__.py
Normal file
@@ -4,6 +4,7 @@ API client functions for interacting with the Airport Announcement System backen
|
|||||||
import requests
|
import requests
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
|
# This can be overridden through environment variables
|
||||||
API_BASE_URL = "http://localhost:7999"
|
API_BASE_URL = "http://localhost:7999"
|
||||||
|
|
||||||
def get_groups() -> List[dict]:
|
def get_groups() -> List[dict]:
|
||||||
0
src/auracaster_webui/__init__.py
Normal file
0
src/auracaster_webui/__init__.py
Normal file
@@ -1,3 +1,6 @@
|
|||||||
|
"""
|
||||||
|
Airport Announcement System Streamlit frontend application.
|
||||||
|
"""
|
||||||
import streamlit as st
|
import streamlit as st
|
||||||
|
|
||||||
# Page setup must be first
|
# Page setup must be first
|
||||||
@@ -5,12 +8,15 @@ st.set_page_config(page_title="Airport Announcement System", page_icon="✈️")
|
|||||||
|
|
||||||
import time
|
import time
|
||||||
import requests
|
import requests
|
||||||
import api_client
|
from api_client.client import (
|
||||||
|
get_groups, get_available_languages, get_announcement_status,
|
||||||
|
start_announcement, update_group, get_available_endpoints
|
||||||
|
)
|
||||||
|
|
||||||
# Initialize session state for configuration
|
# Initialize session state for configuration
|
||||||
if "endpoint_groups" not in st.session_state:
|
if "endpoint_groups" not in st.session_state:
|
||||||
try:
|
try:
|
||||||
st.session_state.endpoint_groups = api_client.get_groups()
|
st.session_state.endpoint_groups = get_groups()
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
st.error(f"Failed to load endpoint groups: {str(e)}")
|
st.error(f"Failed to load endpoint groups: {str(e)}")
|
||||||
st.session_state.endpoint_groups = []
|
st.session_state.endpoint_groups = []
|
||||||
@@ -18,7 +24,7 @@ if "endpoint_groups" not in st.session_state:
|
|||||||
# Initialize session state for available languages
|
# Initialize session state for available languages
|
||||||
if "available_languages" not in st.session_state:
|
if "available_languages" not in st.session_state:
|
||||||
try:
|
try:
|
||||||
st.session_state.available_languages = api_client.get_available_languages()
|
st.session_state.available_languages = get_available_languages()
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
st.error(f"Failed to load available languages: {str(e)}")
|
st.error(f"Failed to load available languages: {str(e)}")
|
||||||
st.session_state.available_languages = ["German", "English"] # Fallback languages
|
st.session_state.available_languages = ["German", "English"] # Fallback languages
|
||||||
@@ -36,7 +42,7 @@ if "status_container_key" not in st.session_state:
|
|||||||
|
|
||||||
def show_announcement_status():
|
def show_announcement_status():
|
||||||
try:
|
try:
|
||||||
status_data = api_client.get_announcement_status()
|
status_data = get_announcement_status()
|
||||||
if status_data["state"] != "Ready":
|
if status_data["state"] != "Ready":
|
||||||
# Create a container with a unique key for each announcement
|
# Create a container with a unique key for each announcement
|
||||||
# This ensures we get a fresh container for each new announcement
|
# This ensures we get a fresh container for each new announcement
|
||||||
@@ -67,7 +73,7 @@ def show_announcement_status():
|
|||||||
time.sleep(0.3)
|
time.sleep(0.3)
|
||||||
|
|
||||||
# Refresh status data
|
# Refresh status data
|
||||||
status_data = api_client.get_announcement_status()
|
status_data = get_announcement_status()
|
||||||
|
|
||||||
# Make sure to update the progress bar one final time with the final state's progress value
|
# Make sure to update the progress bar one final time with the final state's progress value
|
||||||
progress_bar.progress(status_data["progress"])
|
progress_bar.progress(status_data["progress"])
|
||||||
@@ -122,7 +128,7 @@ with st.container():
|
|||||||
if st.form_submit_button("Make Announcement"):
|
if st.form_submit_button("Make Announcement"):
|
||||||
try:
|
try:
|
||||||
selected_group_id = next(g[1] for g in group_options if g[0] == selected_group_name)
|
selected_group_id = next(g[1] for g in group_options if g[0] == selected_group_name)
|
||||||
api_client.start_announcement(message, selected_group_id)
|
start_announcement(message, selected_group_id)
|
||||||
|
|
||||||
# Set flag to show success message
|
# Set flag to show success message
|
||||||
st.session_state.show_success_message = True
|
st.session_state.show_success_message = True
|
||||||
@@ -135,10 +141,6 @@ with st.container():
|
|||||||
st.session_state.announcement_text = ""
|
st.session_state.announcement_text = ""
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
st.error(f"Failed to start announcement: {str(e)}")
|
st.error(f"Failed to start announcement: {str(e)}")
|
||||||
|
|
||||||
# Display success message if flag is set
|
|
||||||
#if st.session_state.show_success_message:
|
|
||||||
# st.success("Announcement started successfully")
|
|
||||||
|
|
||||||
# Configuration section in sidebar
|
# Configuration section in sidebar
|
||||||
with st.sidebar:
|
with st.sidebar:
|
||||||
@@ -167,9 +169,9 @@ with st.sidebar:
|
|||||||
try:
|
try:
|
||||||
updated_group = group.copy()
|
updated_group = group.copy()
|
||||||
updated_group["name"] = new_name
|
updated_group["name"] = new_name
|
||||||
api_client.update_group(group["id"], updated_group)
|
update_group(group["id"], updated_group)
|
||||||
# Update the session state with the latest groups
|
# Update the session state with the latest groups
|
||||||
st.session_state.endpoint_groups = api_client.get_groups()
|
st.session_state.endpoint_groups = get_groups()
|
||||||
# Update the previous value before rerunning
|
# Update the previous value before rerunning
|
||||||
st.session_state[f"prev_{input_key}"] = new_name
|
st.session_state[f"prev_{input_key}"] = new_name
|
||||||
st.rerun()
|
st.rerun()
|
||||||
@@ -177,7 +179,7 @@ with st.sidebar:
|
|||||||
st.error(f"Failed to update group name: {str(e)}")
|
st.error(f"Failed to update group name: {str(e)}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
available_endpoints = api_client.get_available_endpoints()
|
available_endpoints = get_available_endpoints()
|
||||||
# Use a unique key for the endpoints multiselect
|
# Use a unique key for the endpoints multiselect
|
||||||
endpoints_key = f"endpoints_select_{i}"
|
endpoints_key = f"endpoints_select_{i}"
|
||||||
|
|
||||||
@@ -196,66 +198,51 @@ with st.sidebar:
|
|||||||
if selected_endpoints != group["endpoints"] and selected_endpoints != st.session_state[f"prev_{endpoints_key}"]:
|
if selected_endpoints != group["endpoints"] and selected_endpoints != st.session_state[f"prev_{endpoints_key}"]:
|
||||||
updated_group = group.copy()
|
updated_group = group.copy()
|
||||||
updated_group["endpoints"] = selected_endpoints
|
updated_group["endpoints"] = selected_endpoints
|
||||||
api_client.update_group(group["id"], updated_group)
|
update_group(group["id"], updated_group)
|
||||||
# Update the previous value before rerunning
|
# Update the previous value before rerunning
|
||||||
st.session_state[f"prev_{endpoints_key}"] = selected_endpoints
|
st.session_state[f"prev_{endpoints_key}"] = selected_endpoints
|
||||||
|
st.session_state.endpoint_groups = get_groups()
|
||||||
st.rerun()
|
st.rerun()
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
st.error(f"Failed to update endpoints: {str(e)}")
|
st.error(f"Failed to get available endpoints: {str(e)}")
|
||||||
|
|
||||||
with cols[1]:
|
with cols[1]:
|
||||||
if st.button("❌", key=f"remove_{i}"):
|
if st.button("Delete", key=f"delete_group_{i}"):
|
||||||
try:
|
try:
|
||||||
api_client.delete_group(group["id"])
|
# This is assumed to exist in the API client module
|
||||||
del st.session_state.endpoint_groups[i]
|
from api_client.client import delete_group
|
||||||
|
delete_group(group["id"])
|
||||||
|
# Update the session state with the latest groups
|
||||||
|
st.session_state.endpoint_groups = get_groups()
|
||||||
st.rerun()
|
st.rerun()
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
st.error(f"Failed to delete group: {str(e)}")
|
st.error(f"Failed to delete group: {str(e)}")
|
||||||
|
|
||||||
|
# Add new group
|
||||||
|
st.subheader("Add New Group")
|
||||||
|
with st.form("add_group_form"):
|
||||||
|
new_group_name = st.text_input("New Group Name")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Use a unique key for the languages multiselect
|
available_endpoints = get_available_endpoints()
|
||||||
languages_key = f"languages_select_{i}"
|
new_group_endpoints = st.multiselect("Endpoints", options=available_endpoints)
|
||||||
|
|
||||||
# Initialize the previous value in session state if not present
|
|
||||||
if f"prev_{languages_key}" not in st.session_state:
|
|
||||||
st.session_state[f"prev_{languages_key}"] = group["languages"]
|
|
||||||
|
|
||||||
selected_languages = st.multiselect(
|
|
||||||
f"Languages {i+1}",
|
|
||||||
options=st.session_state.available_languages,
|
|
||||||
default=group["languages"],
|
|
||||||
key=languages_key
|
|
||||||
)
|
|
||||||
|
|
||||||
# Only update if languages have changed and they're different from previous value
|
|
||||||
if selected_languages != group["languages"] and selected_languages != st.session_state[f"prev_{languages_key}"]:
|
|
||||||
updated_group = group.copy()
|
|
||||||
updated_group["languages"] = selected_languages
|
|
||||||
api_client.update_group(group["id"], updated_group)
|
|
||||||
# Update the previous value before rerunning
|
|
||||||
st.session_state[f"prev_{languages_key}"] = selected_languages
|
|
||||||
st.rerun()
|
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
st.error(f"Failed to update languages: {str(e)}")
|
st.error(f"Failed to get available endpoints: {str(e)}")
|
||||||
st.markdown("---")
|
new_group_endpoints = []
|
||||||
|
|
||||||
if st.button("➕ Add Group"):
|
if st.form_submit_button("Add Group"):
|
||||||
try:
|
if new_group_name:
|
||||||
new_id = max(g["id"] for g in st.session_state.endpoint_groups) + 1 if st.session_state.endpoint_groups else 1
|
try:
|
||||||
# Get the default languages from the backend (first two languages)
|
from api_client.client import create_group
|
||||||
default_languages = st.session_state.available_languages[:2] if len(st.session_state.available_languages) >= 2 else st.session_state.available_languages
|
new_group = {"name": new_group_name, "endpoints": new_group_endpoints}
|
||||||
new_group = {
|
create_group(new_group)
|
||||||
"id": new_id,
|
# Update the session state with the latest groups
|
||||||
"name": f"Group {len(st.session_state.endpoint_groups)+1}",
|
st.session_state.endpoint_groups = get_groups()
|
||||||
"endpoints": [],
|
st.rerun()
|
||||||
"languages": default_languages
|
except requests.exceptions.RequestException as e:
|
||||||
}
|
st.error(f"Failed to create group: {str(e)}")
|
||||||
created_group = api_client.create_group(new_group)
|
else:
|
||||||
st.session_state.endpoint_groups.append(created_group)
|
st.error("Group name cannot be empty")
|
||||||
st.rerun()
|
|
||||||
except requests.exceptions.RequestException as e:
|
|
||||||
st.error(f"Failed to create group: {str(e)}")
|
|
||||||
|
|
||||||
# Display announcement status
|
# Show the announcement status
|
||||||
st.write("---")
|
|
||||||
show_announcement_status()
|
show_announcement_status()
|
||||||
27
src/auracaster_webui/main_ui.py
Normal file
27
src/auracaster_webui/main_ui.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
"""
|
||||||
|
Main entry point for the Airport Announcement System frontend.
|
||||||
|
"""
|
||||||
|
import streamlit.web.cli as stcli
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
import os
|
||||||
|
|
||||||
|
def run_app():
|
||||||
|
"""Run the Streamlit app."""
|
||||||
|
# Get the directory of this file
|
||||||
|
current_dir = Path(__file__).parent
|
||||||
|
|
||||||
|
# Set the path to the Streamlit app file
|
||||||
|
app_path = current_dir / "app.py"
|
||||||
|
|
||||||
|
# Change directory to the app's directory
|
||||||
|
os.chdir(current_dir)
|
||||||
|
|
||||||
|
# Use sys.argv[0] as the program name
|
||||||
|
sys.argv = ["streamlit", "run", str(app_path)]
|
||||||
|
|
||||||
|
# Run the Streamlit CLI
|
||||||
|
sys.exit(stcli.main())
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
run_app()
|
||||||
0
src/mock_backend/__init__.py
Normal file
0
src/mock_backend/__init__.py
Normal file
@@ -1,6 +1,18 @@
|
|||||||
|
"""
|
||||||
|
FastAPI implementation of the Airport Announcement System mock backend API.
|
||||||
|
"""
|
||||||
from fastapi import FastAPI, HTTPException
|
from fastapi import FastAPI, HTTPException
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Add the src directory to the Python path
|
||||||
|
src_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
|
||||||
|
if src_path not in sys.path:
|
||||||
|
sys.path.insert(0, src_path)
|
||||||
|
|
||||||
|
# Use absolute imports instead of relative imports
|
||||||
from mock_backend.mock_backend import announcement_system
|
from mock_backend.mock_backend import announcement_system
|
||||||
from api_models import EndpointGroup
|
from api_client.models import EndpointGroup
|
||||||
from typing import List
|
from typing import List
|
||||||
import uvicorn
|
import uvicorn
|
||||||
|
|
||||||
238
src/mock_backend/mock_backend.py
Normal file
238
src/mock_backend/mock_backend.py
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
"""
|
||||||
|
Mock implementation of the Airport Announcement System backend.
|
||||||
|
"""
|
||||||
|
import time
|
||||||
|
import threading
|
||||||
|
from typing import Optional, List
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Add the src directory to the Python path
|
||||||
|
src_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
|
||||||
|
if src_path not in sys.path:
|
||||||
|
sys.path.insert(0, src_path)
|
||||||
|
|
||||||
|
from api_client.models import EndpointGroup, AnnouncementStates
|
||||||
|
|
||||||
|
AVAILABLE_ENDPOINTS = [f"endpoint{i}" for i in range(1, 4)] # Predefined endpoints
|
||||||
|
AVAILABLE_LANGUAGES = ["German", "English", "French", "Spanish", "Italian"]
|
||||||
|
|
||||||
|
class AnnouncementSystem:
|
||||||
|
def __init__(self):
|
||||||
|
self.available_endpoints = AVAILABLE_ENDPOINTS
|
||||||
|
self.available_languages = AVAILABLE_LANGUAGES
|
||||||
|
self.endpoint_groups = [
|
||||||
|
EndpointGroup(
|
||||||
|
id=1,
|
||||||
|
name="Gate1",
|
||||||
|
endpoints=["endpoint1", "endpoint2"],
|
||||||
|
languages=["German", "English"]
|
||||||
|
),
|
||||||
|
EndpointGroup(
|
||||||
|
id=2,
|
||||||
|
name="Gate2",
|
||||||
|
endpoints=["endpoint3"],
|
||||||
|
languages=["German", "English"]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
self.active_group_id = None
|
||||||
|
self._thread = None
|
||||||
|
self.last_completed_group_id = None
|
||||||
|
|
||||||
|
def get_endpoint_groups(self) -> List[EndpointGroup]:
|
||||||
|
"""Get all endpoint groups."""
|
||||||
|
return self.endpoint_groups
|
||||||
|
|
||||||
|
def get_endpoint_group(self, group_id: int) -> Optional[EndpointGroup]:
|
||||||
|
"""Get a specific endpoint group by ID."""
|
||||||
|
for group in self.endpoint_groups:
|
||||||
|
if group.id == group_id:
|
||||||
|
return group
|
||||||
|
return None
|
||||||
|
|
||||||
|
def add_endpoint_group(self, group: EndpointGroup) -> EndpointGroup:
|
||||||
|
"""Add a new endpoint group."""
|
||||||
|
# Ensure group with this ID doesn't already exist
|
||||||
|
for existing_group in self.endpoint_groups:
|
||||||
|
if existing_group.id == group.id:
|
||||||
|
raise ValueError(f"Group with ID {group.id} already exists")
|
||||||
|
|
||||||
|
# If no ID is provided or ID is 0, auto-assign the next available ID
|
||||||
|
if group.id == 0:
|
||||||
|
max_id = max([g.id for g in self.endpoint_groups]) if self.endpoint_groups else 0
|
||||||
|
group.id = max_id + 1
|
||||||
|
|
||||||
|
self.endpoint_groups.append(group)
|
||||||
|
return group
|
||||||
|
|
||||||
|
def update_endpoint_group(self, group_id: int, updated_group: EndpointGroup) -> EndpointGroup:
|
||||||
|
"""Update an existing endpoint group."""
|
||||||
|
for i, group in enumerate(self.endpoint_groups):
|
||||||
|
if group.id == group_id:
|
||||||
|
# Ensure the ID doesn't change
|
||||||
|
updated_group.id = group_id
|
||||||
|
self.endpoint_groups[i] = updated_group
|
||||||
|
return updated_group
|
||||||
|
|
||||||
|
raise ValueError(f"Group with ID {group_id} not found")
|
||||||
|
|
||||||
|
def delete_endpoint_group(self, group_id: int) -> None:
|
||||||
|
"""Delete an endpoint group."""
|
||||||
|
for i, group in enumerate(self.endpoint_groups):
|
||||||
|
if group.id == group_id:
|
||||||
|
del self.endpoint_groups[i]
|
||||||
|
return
|
||||||
|
|
||||||
|
raise ValueError(f"Group with ID {group_id} not found")
|
||||||
|
|
||||||
|
def get_available_endpoints(self) -> List[str]:
|
||||||
|
"""Get all available endpoints."""
|
||||||
|
return self.available_endpoints
|
||||||
|
|
||||||
|
def get_available_languages(self) -> List[str]:
|
||||||
|
"""Get all available languages for announcements."""
|
||||||
|
return self.available_languages
|
||||||
|
|
||||||
|
def _simulate_announcement(self, text: str, group: EndpointGroup) -> None:
|
||||||
|
"""
|
||||||
|
Simulate an announcement being made.
|
||||||
|
This runs in a separate thread to allow the API to continue serving requests.
|
||||||
|
"""
|
||||||
|
# Set start time
|
||||||
|
group.parameters.text = text
|
||||||
|
group.parameters.languages = ["German", "English"] # Default languages
|
||||||
|
group.parameters.start_time = time.time()
|
||||||
|
|
||||||
|
# Track the active group
|
||||||
|
self.active_group_id = group.id
|
||||||
|
|
||||||
|
# Update status to translating
|
||||||
|
group.progress.current_state = AnnouncementStates.TRANSLATING.value
|
||||||
|
group.progress.progress = 0.2
|
||||||
|
time.sleep(1.5) # Simulate translation time
|
||||||
|
|
||||||
|
# Check if we should stop (e.g. if another announcement started)
|
||||||
|
if self.active_group_id != group.id:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Update status to generating voice
|
||||||
|
group.progress.current_state = AnnouncementStates.GENERATING_VOICE.value
|
||||||
|
group.progress.progress = 0.4
|
||||||
|
time.sleep(2) # Simulate voice generation time
|
||||||
|
|
||||||
|
# Check if we should stop
|
||||||
|
if self.active_group_id != group.id:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Update status to routing
|
||||||
|
group.progress.current_state = AnnouncementStates.ROUTING.value
|
||||||
|
group.progress.progress = 0.6
|
||||||
|
time.sleep(1) # Simulate routing time
|
||||||
|
|
||||||
|
# Check if we should stop
|
||||||
|
if self.active_group_id != group.id:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Update status to active
|
||||||
|
group.progress.current_state = AnnouncementStates.ACTIVE.value
|
||||||
|
group.progress.progress = 0.8
|
||||||
|
time.sleep(2.5) # Simulate announcement playing time
|
||||||
|
|
||||||
|
# Check if we should stop
|
||||||
|
if self.active_group_id != group.id:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Update status to complete
|
||||||
|
group.progress.current_state = AnnouncementStates.COMPLETE.value
|
||||||
|
group.progress.progress = 1.0
|
||||||
|
|
||||||
|
# Record the last completed group
|
||||||
|
self.last_completed_group_id = group.id
|
||||||
|
|
||||||
|
# Reset active group if this is still the active one
|
||||||
|
if self.active_group_id == group.id:
|
||||||
|
self.active_group_id = None
|
||||||
|
|
||||||
|
# After a while, reset to idle state
|
||||||
|
def reset_to_idle():
|
||||||
|
time.sleep(10) # Keep completed state visible for 10 seconds
|
||||||
|
if group.progress.current_state == AnnouncementStates.COMPLETE.value:
|
||||||
|
group.progress.current_state = AnnouncementStates.IDLE.value
|
||||||
|
group.progress.progress = 0.0
|
||||||
|
|
||||||
|
reset_thread = threading.Thread(target=reset_to_idle)
|
||||||
|
reset_thread.daemon = True
|
||||||
|
reset_thread.start()
|
||||||
|
|
||||||
|
def start_announcement(self, text: str, group: EndpointGroup) -> None:
|
||||||
|
"""Start a new announcement to the specified endpoint group."""
|
||||||
|
# Check if an announcement is already in progress
|
||||||
|
if self.active_group_id is not None:
|
||||||
|
# Cancel the current announcement
|
||||||
|
self.active_group_id = None
|
||||||
|
|
||||||
|
# Start a new thread to handle the announcement
|
||||||
|
self._thread = threading.Thread(target=self._simulate_announcement, args=(text, group))
|
||||||
|
self._thread.daemon = True
|
||||||
|
self._thread.start()
|
||||||
|
|
||||||
|
def get_announcement_status(self) -> dict:
|
||||||
|
"""Get the status of the current announcement."""
|
||||||
|
# If an announcement is active, return its status
|
||||||
|
if self.active_group_id is not None:
|
||||||
|
group = self.get_endpoint_group(self.active_group_id)
|
||||||
|
return {
|
||||||
|
"state": group.progress.current_state,
|
||||||
|
"progress": group.progress.progress,
|
||||||
|
"error": group.progress.error,
|
||||||
|
"details": {
|
||||||
|
"group": {
|
||||||
|
"id": group.id,
|
||||||
|
"name": group.name,
|
||||||
|
"endpoints": group.endpoints
|
||||||
|
},
|
||||||
|
"text": group.parameters.text,
|
||||||
|
"languages": group.parameters.languages,
|
||||||
|
"start_time": group.parameters.start_time
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# If no announcement is active but we have a last completed one
|
||||||
|
elif self.last_completed_group_id is not None:
|
||||||
|
group = self.get_endpoint_group(self.last_completed_group_id)
|
||||||
|
if group and group.progress.current_state == AnnouncementStates.COMPLETE.value:
|
||||||
|
return {
|
||||||
|
"state": group.progress.current_state,
|
||||||
|
"progress": group.progress.progress,
|
||||||
|
"error": group.progress.error,
|
||||||
|
"details": {
|
||||||
|
"group": {
|
||||||
|
"id": group.id,
|
||||||
|
"name": group.name,
|
||||||
|
"endpoints": group.endpoints
|
||||||
|
},
|
||||||
|
"text": group.parameters.text,
|
||||||
|
"languages": group.parameters.languages,
|
||||||
|
"start_time": group.parameters.start_time
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Default: no active announcement
|
||||||
|
return {
|
||||||
|
"state": AnnouncementStates.IDLE.value,
|
||||||
|
"progress": 0.0,
|
||||||
|
"error": None,
|
||||||
|
"details": {
|
||||||
|
"group": {
|
||||||
|
"id": 0,
|
||||||
|
"name": "",
|
||||||
|
"endpoints": []
|
||||||
|
},
|
||||||
|
"text": "",
|
||||||
|
"languages": [],
|
||||||
|
"start_time": time.time()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Singleton instance
|
||||||
|
announcement_system = AnnouncementSystem()
|
||||||
Reference in New Issue
Block a user