restructure the project for packaging

This commit is contained in:
2025-03-11 11:32:26 +01:00
parent f3bdb6d53f
commit 1485277d66
16 changed files with 489 additions and 294 deletions

View File

62
src/api_client/client.py Normal file
View File

@@ -0,0 +1,62 @@
"""
API client functions for interacting with the Airport Announcement System backend.
"""
import requests
from typing import List, Optional
# This can be overridden through environment variables
API_BASE_URL = "http://localhost:7999"
def get_groups() -> List[dict]:
"""Get all endpoint groups."""
response = requests.get(f"{API_BASE_URL}/groups")
response.raise_for_status()
return response.json()
def get_group(group_id: int) -> Optional[dict]:
"""Get a specific endpoint group by ID."""
response = requests.get(f"{API_BASE_URL}/groups/{group_id}")
if response.status_code == 404:
return None
response.raise_for_status()
return response.json()
def create_group(group: dict) -> dict:
"""Create a new endpoint group."""
response = requests.post(f"{API_BASE_URL}/groups", json=group)
response.raise_for_status()
return response.json()
def update_group(group_id: int, updated_group: dict) -> dict:
"""Update an existing endpoint group."""
response = requests.put(f"{API_BASE_URL}/groups/{group_id}", json=updated_group)
response.raise_for_status()
return response.json()
def delete_group(group_id: int) -> None:
"""Delete an endpoint group."""
response = requests.delete(f"{API_BASE_URL}/groups/{group_id}")
response.raise_for_status()
def start_announcement(text: str, group_id: int) -> None:
"""Start a new announcement."""
response = requests.post(f"{API_BASE_URL}/announcements", params={"text": text, "group_id": group_id})
response.raise_for_status()
def get_announcement_status() -> dict:
"""Get the status of the current announcement."""
response = requests.get(f"{API_BASE_URL}/announcements/status")
response.raise_for_status()
return response.json()
def get_available_endpoints() -> List[str]:
"""Get all available endpoints."""
response = requests.get(f"{API_BASE_URL}/endpoints")
response.raise_for_status()
return response.json()
def get_available_languages() -> List[str]:
"""Get all available languages for announcements."""
response = requests.get(f"{API_BASE_URL}/languages")
response.raise_for_status()
return response.json()

35
src/api_client/models.py Normal file
View File

@@ -0,0 +1,35 @@
"""
API models for the Airport Announcement System.
"""
from enum import Enum
from pydantic import BaseModel, Field
from typing import Optional, List
class AnnouncementStates(Enum):
IDLE: str = "Ready"
TRANSLATING: str = "Translating"
GENERATING_VOICE: str = "Generating voice synthesis"
ROUTING: str = "Routing to endpoints"
ACTIVE: str = "Broadcasting announcement"
COMPLETE: str = "Complete"
ERROR: str = "Error"
class AnnouncementParameters(BaseModel):
text: Optional[str] = None
languages: List[str] = []
start_time: Optional[float] = None
class AnnouncementProgress(BaseModel):
current_state: str = AnnouncementStates.IDLE.value
progress: float = Field(default=0.0, ge=0.0, le=1.0)
error: Optional[str] = None
class EndpointGroup(BaseModel):
id: int
name: str
endpoints: List[str]
languages: List[str]
# Announcement parameters and progress as nested models
parameters: AnnouncementParameters = Field(default_factory=AnnouncementParameters)
progress: AnnouncementProgress = Field(default_factory=AnnouncementProgress)

View File

248
src/auracaster_webui/app.py Normal file
View File

@@ -0,0 +1,248 @@
"""
Airport Announcement System Streamlit frontend application.
"""
import streamlit as st
# Page setup must be first
st.set_page_config(page_title="Airport Announcement System", page_icon="✈️")
import time
import requests
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
if "endpoint_groups" not in st.session_state:
try:
st.session_state.endpoint_groups = get_groups()
except requests.exceptions.RequestException as e:
st.error(f"Failed to load endpoint groups: {str(e)}")
st.session_state.endpoint_groups = []
# Initialize session state for available languages
if "available_languages" not in st.session_state:
try:
st.session_state.available_languages = get_available_languages()
except requests.exceptions.RequestException as e:
st.error(f"Failed to load available languages: {str(e)}")
st.session_state.available_languages = ["German", "English"] # Fallback languages
# Initialize session state for announcement text and status tracking
if "announcement_text" not in st.session_state:
st.session_state.announcement_text = "Hallo Welt."
if "show_success_message" not in st.session_state:
st.session_state.show_success_message = False
if "announcement_id" not in st.session_state:
st.session_state.announcement_id = 0
if "status_container_key" not in st.session_state:
st.session_state.status_container_key = 0
def show_announcement_status():
try:
status_data = get_announcement_status()
if status_data["state"] != "Ready":
# Create a container with a unique key for each announcement
# This ensures we get a fresh container for each new announcement
with st.container(key=f"status_container_{st.session_state.status_container_key}"):
# Create the status without a key parameter
status = st.status("**Airport PA System Status**", expanded=True)
with status:
# Progress elements
progress_bar = st.progress(status_data["progress"])
time_col, stage_col = st.columns([1, 3])
# Track last displayed state
last_state = None
# Update loop
while status_data["state"] not in ["Complete", "Error"]:
# Update time elapsed continuously
# Only update stage display if state changed
if status_data["state"] != last_state:
stage_col.write(f"**Stage:** {status_data['state']}")
last_state = status_data["state"]
time_col.write(f"⏱️ Time elapsed: {time.time() - status_data['details']['start_time']:.1f}s")
# Update progress bar
progress_bar.progress(status_data["progress"])
time.sleep(0.3)
# Refresh status data
status_data = get_announcement_status()
# Make sure to update the progress bar one final time with the final state's progress value
progress_bar.progress(status_data["progress"])
# Final state
if status_data["state"] == "Error":
st.error(f"❌ Error: {status_data['error']}")
else:
st.success("✅ Announcement completed successfully")
st.write(f"📢 Announcement made to group {status_data['details']['group']['name']}:")
st.write(f"📡 Endpoints: {', '.join(status_data['details']['group']['endpoints'])}")
st.write(f"🗣️ '{status_data['details']['text']}'")
st.write(f"🌐 Languages: {', '.join(status_data['details']['languages'])}")
# Clear the success message when announcement completes
st.session_state.show_success_message = False
except requests.exceptions.RequestException as e:
st.error(f"Failed to get announcement status: {str(e)}")
# Main interface
st.title("Airport Announcement System ✈️")
# Announcements section
with st.container():
st.header("Announcements")
# Predefined announcements
st.write("**Predefined Announcements** (click to autofill)")
col1, col2, col3 = st.columns(3)
with col1:
if st.button("Final Boarding Call"):
st.session_state.announcement_text = "This is the final boarding call for flight LX-380 to New York"
with col2:
if st.button("Security Reminder"):
st.session_state.announcement_text = "Please keep your luggage with you at all times"
with col3:
if st.button("Delay Notice"):
st.session_state.announcement_text = "We regret to inform you of a 30-minute delay"
# Custom announcement
with st.form("custom_announcement"):
# Get all groups with their names and IDs
group_options = [(g["name"], g["id"]) for g in st.session_state.endpoint_groups]
selected_group_name = st.selectbox(
"Select announcement area",
options=[g[0] for g in group_options]
)
message = st.text_area("Enter announcement text", st.session_state.announcement_text)
if st.form_submit_button("Make Announcement"):
try:
selected_group_id = next(g[1] for g in group_options if g[0] == selected_group_name)
start_announcement(message, selected_group_id)
# Set flag to show success message
st.session_state.show_success_message = True
# Increment announcement ID to ensure a fresh status container
st.session_state.announcement_id += 1
st.session_state.status_container_key = st.session_state.announcement_id
# Clear the announcement text after successful submission
st.session_state.announcement_text = ""
except requests.exceptions.RequestException as e:
st.error(f"Failed to start announcement: {str(e)}")
# Configuration section in sidebar
with st.sidebar:
st.header("Configuration")
with st.expander("Endpoint Groups"):
for i, group in enumerate(st.session_state.endpoint_groups):
cols = st.columns([4, 1])
with cols[0]:
# Use a unique key for the text input
input_key = f"group_name_{i}"
# Initialize the previous value in session state if not present
if f"prev_{input_key}" not in st.session_state:
st.session_state[f"prev_{input_key}"] = group["name"]
new_name = st.text_input(
f"Group Name",
value=group["name"],
key=input_key,
on_change=lambda: None # Prevent automatic callbacks
)
# Only update if the name has changed and it's different from the previous value
if new_name != group["name"] and new_name != st.session_state[f"prev_{input_key}"]:
try:
updated_group = group.copy()
updated_group["name"] = new_name
update_group(group["id"], updated_group)
# Update the session state with the latest groups
st.session_state.endpoint_groups = get_groups()
# Update the previous value before rerunning
st.session_state[f"prev_{input_key}"] = new_name
st.rerun()
except requests.exceptions.RequestException as e:
st.error(f"Failed to update group name: {str(e)}")
try:
available_endpoints = get_available_endpoints()
# Use a unique key for the endpoints multiselect
endpoints_key = f"endpoints_select_{i}"
# Initialize the previous value in session state if not present
if f"prev_{endpoints_key}" not in st.session_state:
st.session_state[f"prev_{endpoints_key}"] = group["endpoints"]
selected_endpoints = st.multiselect(
f"Endpoints",
options=available_endpoints,
default=group["endpoints"],
key=endpoints_key
)
# Only update if endpoints have changed and they're different from previous value
if selected_endpoints != group["endpoints"] and selected_endpoints != st.session_state[f"prev_{endpoints_key}"]:
updated_group = group.copy()
updated_group["endpoints"] = selected_endpoints
update_group(group["id"], updated_group)
# Update the previous value before rerunning
st.session_state[f"prev_{endpoints_key}"] = selected_endpoints
st.session_state.endpoint_groups = get_groups()
st.rerun()
except requests.exceptions.RequestException as e:
st.error(f"Failed to get available endpoints: {str(e)}")
with cols[1]:
if st.button("Delete", key=f"delete_group_{i}"):
try:
# This is assumed to exist in the API client module
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()
except requests.exceptions.RequestException as 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:
available_endpoints = get_available_endpoints()
new_group_endpoints = st.multiselect("Endpoints", options=available_endpoints)
except requests.exceptions.RequestException as e:
st.error(f"Failed to get available endpoints: {str(e)}")
new_group_endpoints = []
if st.form_submit_button("Add Group"):
if new_group_name:
try:
from api_client.client import create_group
new_group = {"name": new_group_name, "endpoints": new_group_endpoints}
create_group(new_group)
# Update the session state with the latest groups
st.session_state.endpoint_groups = get_groups()
st.rerun()
except requests.exceptions.RequestException as e:
st.error(f"Failed to create group: {str(e)}")
else:
st.error("Group name cannot be empty")
# Show the announcement status
show_announcement_status()

View 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()

View File

View File

@@ -0,0 +1,72 @@
"""
FastAPI implementation of the Airport Announcement System mock backend API.
"""
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 api_client.models import EndpointGroup
from typing import List
import uvicorn
app = FastAPI()
@app.get("/groups", response_model=List[EndpointGroup])
def get_groups():
return announcement_system.get_endpoint_groups()
@app.post("/groups", response_model=EndpointGroup)
def create_group(group: EndpointGroup):
try:
return announcement_system.add_endpoint_group(group)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
@app.put("/groups/{group_id}", response_model=EndpointGroup)
def update_group(group_id: int, updated_group: EndpointGroup):
try:
return announcement_system.update_endpoint_group(group_id, updated_group)
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
@app.delete("/groups/{group_id}")
def delete_group(group_id: int):
try:
announcement_system.delete_endpoint_group(group_id)
return {"message": "Group deleted successfully"}
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
@app.post("/announcements")
def start_announcement(text: str, group_id: int):
group = announcement_system.get_endpoint_group(group_id)
if not group:
raise HTTPException(status_code=404, detail=f"Group with ID {group_id} not found")
try:
announcement_system.start_announcement(text, group)
return {"message": "Announcement started successfully"}
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
@app.get("/announcements/status")
def get_announcement_status():
return announcement_system.get_announcement_status()
@app.get("/endpoints")
def get_available_endpoints():
return announcement_system.get_available_endpoints()
@app.get("/languages")
def get_available_languages():
return announcement_system.get_available_languages()
if __name__ == "__main__":
uvicorn.run('mock_backend.mock_api:app', host="0.0.0.0", port=7999, reload=True)

View 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()