diff --git a/api_client.py b/api_client.py new file mode 100644 index 0000000..490e0ef --- /dev/null +++ b/api_client.py @@ -0,0 +1,61 @@ +""" +API client functions for interacting with the Airport Announcement System backend. +""" +import requests +from typing import List, Optional + +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() diff --git a/api_models.py b/api_models.py new file mode 100644 index 0000000..619e35e --- /dev/null +++ b/api_models.py @@ -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) diff --git a/auracaster-webui.py b/auracaster-webui.py index 2bbc8e1..5248f41 100644 --- a/auracaster-webui.py +++ b/auracaster-webui.py @@ -3,58 +3,9 @@ import streamlit as st # Page setup must be first st.set_page_config(page_title="Airport Announcement System", page_icon="✈️") -import requests import time -from typing import List, Optional - -API_BASE_URL = "http://localhost:7999" - -class APIClient: - def get_groups(self) -> List[dict]: - response = requests.get(f"{API_BASE_URL}/groups") - response.raise_for_status() - return response.json() - - def get_group(self, group_id: int) -> Optional[dict]: - 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(self, group: dict) -> dict: - response = requests.post(f"{API_BASE_URL}/groups", json=group) - response.raise_for_status() - return response.json() - - def update_group(self, group_id: int, updated_group: dict) -> dict: - response = requests.put(f"{API_BASE_URL}/groups/{group_id}", json=updated_group) - response.raise_for_status() - return response.json() - - def delete_group(self, group_id: int) -> None: - response = requests.delete(f"{API_BASE_URL}/groups/{group_id}") - response.raise_for_status() - - def start_announcement(self, text: str, group_id: int) -> None: - response = requests.post(f"{API_BASE_URL}/announcements", params={"text": text, "group_id": group_id}) - response.raise_for_status() - - def get_announcement_status(self) -> dict: - response = requests.get(f"{API_BASE_URL}/announcements/status") - response.raise_for_status() - return response.json() - - def get_available_endpoints(self) -> List[str]: - response = requests.get(f"{API_BASE_URL}/endpoints") - response.raise_for_status() - return response.json() - -api_client = APIClient() - -# Configuration defaults -DEFAULT_LANGUAGES = ["German", "English"] -OPTIONAL_LANGUAGES = ["French", "Spanish"] +import requests +import api_client # Initialize session state for configuration if "endpoint_groups" not in st.session_state: @@ -64,6 +15,14 @@ if "endpoint_groups" not in st.session_state: 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 = api_client.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." @@ -178,8 +137,8 @@ with st.container(): 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") + #if st.session_state.show_success_message: + # st.success("Announcement started successfully") # Configuration section in sidebar with st.sidebar: @@ -263,7 +222,7 @@ with st.sidebar: selected_languages = st.multiselect( f"Languages {i+1}", - options=DEFAULT_LANGUAGES + OPTIONAL_LANGUAGES, + options=st.session_state.available_languages, default=group["languages"], key=languages_key ) @@ -283,11 +242,13 @@ with st.sidebar: if st.button("➕ Add Group"): try: new_id = max(g["id"] for g in st.session_state.endpoint_groups) + 1 if st.session_state.endpoint_groups else 1 + # Get the default languages from the backend (first two languages) + default_languages = st.session_state.available_languages[:2] if len(st.session_state.available_languages) >= 2 else st.session_state.available_languages new_group = { "id": new_id, "name": f"Group {len(st.session_state.endpoint_groups)+1}", "endpoints": [], - "languages": DEFAULT_LANGUAGES.copy() + "languages": default_languages } created_group = api_client.create_group(new_group) st.session_state.endpoint_groups.append(created_group) diff --git a/main_mock.py b/main_mock.py index 042b787..4f79497 100644 --- a/main_mock.py +++ b/main_mock.py @@ -6,8 +6,18 @@ import subprocess import sys import time import os +import threading from pathlib import Path +def stream_output(process, prefix): + """Stream the output of a process to the console in real-time.""" + for line in iter(process.stdout.readline, ''): + if line: + print(f"{prefix}: {line.strip()}") + for line in iter(process.stderr.readline, ''): + if line: + print(f"{prefix} ERROR: {line.strip()}") + def start_backend(): """Start the backend API server.""" print("Starting backend API server...") @@ -15,8 +25,14 @@ def start_backend(): [sys.executable, "-m", "uvicorn", "mock_backend.mock_api:app", "--host", "0.0.0.0", "--port", "7999", "--reload"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, - text=True + text=True, + bufsize=1 # Line buffered ) + + # Start a thread to stream the backend output + 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) return backend_process @@ -28,8 +44,14 @@ def start_frontend(): [sys.executable, "-m", "streamlit", "run", "auracaster-webui.py"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, - text=True + text=True, + bufsize=1 # Line buffered ) + + # Start a thread to stream the frontend output + frontend_thread = threading.Thread(target=stream_output, args=(frontend_process, "FRONTEND"), daemon=True) + frontend_thread.start() + return frontend_process if __name__ == "__main__": diff --git a/mock_backend/mock_api.py b/mock_backend/mock_api.py index f4bd04e..534e7ad 100644 --- a/mock_backend/mock_api.py +++ b/mock_backend/mock_api.py @@ -1,5 +1,6 @@ from fastapi import FastAPI, HTTPException -from mock_backend.mock_backend import announcement_system, EndpointGroup +from mock_backend.mock_backend import announcement_system +from api_models import EndpointGroup from typing import List import uvicorn @@ -45,22 +46,15 @@ def start_announcement(text: str, group_id: int): @app.get("/announcements/status") def get_announcement_status(): - process = announcement_system.current_process - return { - "state": process.current_state.value, - "progress": process.progress, - "error": process.error, - "details": { - "text": process.details.text, - "languages": process.details.languages, - "group": process.details.group, - "start_time": process.details.start_time - } - } + return announcement_system.get_announcement_status() @app.get("/endpoints") def get_available_endpoints(): - return announcement_system.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) diff --git a/mock_backend/mock_backend.py b/mock_backend/mock_backend.py index 5595882..d456d61 100644 --- a/mock_backend/mock_backend.py +++ b/mock_backend/mock_backend.py @@ -1,42 +1,16 @@ import time import threading -from enum import Enum -from pydantic import BaseModel, Field from typing import Optional, List -ENDPOINTS = [f"endpoint{i}" for i in range(1, 4)] # Predefined endpoints +from api_models import EndpointGroup, AnnouncementStates -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 EndpointGroup(BaseModel): - id: int - name: str - endpoints: List[str] - languages: List[str] - -class AnnouncementDetails(BaseModel): - text: str - languages: List[str] - group: EndpointGroup - start_time: float - -class AnnouncementProgress(BaseModel): - current_state: str = AnnouncementStates.IDLE - error: Optional[str] = None - details: AnnouncementDetails - progress: float = Field(default=0.0, ge=0.0, le=1.0) - description: str = Field(default="Ready") +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 = ENDPOINTS + self.available_endpoints = AVAILABLE_ENDPOINTS + self.available_languages = AVAILABLE_LANGUAGES self.endpoint_groups = [ EndpointGroup( id=1, @@ -51,56 +25,69 @@ class AnnouncementSystem: languages=["German", "English"] ) ] - self.current_process = AnnouncementProgress( - details=AnnouncementDetails( - text="", - languages=[], - group=EndpointGroup( - id=0, - name="", - endpoints=[], - languages=[] - ), - start_time=0.0 - ) - ) + self.active_group_id = None self._thread = None - + self.last_completed_group_id = None - def start_announcement(self, text, group): - if self.current_process.current_state not in [AnnouncementStates.IDLE, AnnouncementStates.COMPLETE]: - raise Exception("Announcement already in progress") + 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") - self.current_process.details.text = text - self.current_process.details.group = group - # Set the languages from the group - self.current_process.details.languages = group.languages - self._thread = threading.Thread(target=self._run_process) + # 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): - self.current_process.details.start_time = time.time() + def _run_process(self, group: EndpointGroup): try: - self._update_state(AnnouncementStates.TRANSLATING) + self._update_state(group, AnnouncementStates.TRANSLATING) time.sleep(1) # Simulate translation - self._update_state(AnnouncementStates.GENERATING_VOICE) + self._update_state(group, AnnouncementStates.GENERATING_VOICE) time.sleep(1) # Voice synthesis - self._update_state(AnnouncementStates.ROUTING) - time.sleep(len(self.current_process.details.group.endpoints) * 0.5) + self._update_state(group, AnnouncementStates.ROUTING) + time.sleep(len(group.endpoints) * 0.5) - self._update_state(AnnouncementStates.ACTIVE) + self._update_state(group, AnnouncementStates.ACTIVE) time.sleep(1) # Simulate broadcast - self._update_state(AnnouncementStates.COMPLETE) + self._update_state(group, AnnouncementStates.COMPLETE) + self.last_completed_group_id = group.id + self.active_group_id = None except Exception as e: - self.current_process.error = str(e) - self._update_state(AnnouncementStates.ERROR) + group.progress.error = str(e) + self._update_state(group, AnnouncementStates.ERROR) + self.active_group_id = None - def _update_state(self, new_state): - self.current_process.current_state = new_state + def _update_state(self, group: EndpointGroup, new_state: AnnouncementStates): + group.progress.current_state = new_state.value # Progress based on state transitions state_progress = { @@ -111,7 +98,7 @@ class AnnouncementSystem: AnnouncementStates.COMPLETE: 1.0, AnnouncementStates.ERROR: 0 } - self.current_process.progress = state_progress[new_state] + group.progress.progress = state_progress[new_state] def get_endpoint_groups(self) -> List[EndpointGroup]: return self.endpoint_groups @@ -133,6 +120,7 @@ class AnnouncementSystem: 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 @@ -142,7 +130,78 @@ class AnnouncementSystem: 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()