Fix LLM model list, Castopod API, and server runner

- Remove gpt-4o-realtime (WebSocket-only) from OpenRouter models
- Increase OpenRouter timeout to 60s and max_tokens to 150
- Handle empty LLM responses
- Fix publish_episode.py for current Castopod API fields
- Add port conflict check and graceful shutdown to run.sh

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-06 01:56:09 -07:00
parent a94fc92647
commit 7adf1bbcad
3 changed files with 64 additions and 21 deletions

View File

@@ -7,10 +7,10 @@ from ..config import settings
# Available OpenRouter models # Available OpenRouter models
OPENROUTER_MODELS = [ OPENROUTER_MODELS = [
"anthropic/claude-3-haiku",
"anthropic/claude-3.5-sonnet",
"openai/gpt-4o-mini", "openai/gpt-4o-mini",
"openai/gpt-4o", "openai/gpt-4o",
"anthropic/claude-3-haiku",
"anthropic/claude-3.5-sonnet",
"google/gemini-flash-1.5", "google/gemini-flash-1.5",
"google/gemini-pro-1.5", "google/gemini-pro-1.5",
"meta-llama/llama-3.1-8b-instruct", "meta-llama/llama-3.1-8b-instruct",
@@ -114,7 +114,7 @@ class LLMService:
"""Call OpenRouter API with retry""" """Call OpenRouter API with retry"""
for attempt in range(2): # Try twice for attempt in range(2): # Try twice
try: try:
async with httpx.AsyncClient(timeout=30.0) as client: async with httpx.AsyncClient(timeout=60.0) as client:
response = await client.post( response = await client.post(
"https://openrouter.ai/api/v1/chat/completions", "https://openrouter.ai/api/v1/chat/completions",
headers={ headers={
@@ -124,12 +124,16 @@ class LLMService:
json={ json={
"model": self.openrouter_model, "model": self.openrouter_model,
"messages": messages, "messages": messages,
"max_tokens": 100, "max_tokens": 150,
}, },
) )
response.raise_for_status() response.raise_for_status()
data = response.json() data = response.json()
return data["choices"][0]["message"]["content"] content = data["choices"][0]["message"]["content"]
if not content or not content.strip():
print(f"OpenRouter returned empty response")
return ""
return content
except (httpx.TimeoutException, httpx.ReadTimeout): except (httpx.TimeoutException, httpx.ReadTimeout):
print(f"OpenRouter timeout (attempt {attempt + 1})") print(f"OpenRouter timeout (attempt {attempt + 1})")
if attempt == 0: if attempt == 0:

View File

@@ -155,11 +155,12 @@ Respond with ONLY valid JSON, no markdown or explanation."""
return metadata return metadata
def create_episode(audio_path: str, metadata: dict, duration: int) -> dict: def create_episode(audio_path: str, metadata: dict, episode_number: int) -> dict:
"""Create episode on Castopod.""" """Create episode on Castopod."""
print("[3/5] Creating episode on Castopod...") print("[3/5] Creating episode on Castopod...")
headers = get_auth_header() headers = get_auth_header()
slug = re.sub(r'[^a-z0-9]+', '-', metadata["title"].lower()).strip('-')
# Upload audio and create episode # Upload audio and create episode
with open(audio_path, "rb") as f: with open(audio_path, "rb") as f:
@@ -168,21 +169,25 @@ def create_episode(audio_path: str, metadata: dict, duration: int) -> dict:
} }
data = { data = {
"title": metadata["title"], "title": metadata["title"],
"description_markdown": metadata["description"], "slug": slug,
"description": metadata["description"],
"parental_advisory": "explicit", "parental_advisory": "explicit",
"type": "full", "type": "full",
"created_by": "1" "podcast_id": str(PODCAST_ID),
"created_by": "1",
"updated_by": "1",
"episode_number": str(episode_number),
} }
response = requests.post( response = requests.post(
f"{CASTOPOD_URL}/api/rest/v1/podcasts/{PODCAST_ID}/episodes", f"{CASTOPOD_URL}/api/rest/v1/episodes",
headers=headers, headers=headers,
files=files, files=files,
data=data data=data
) )
if response.status_code not in (200, 201): if response.status_code not in (200, 201):
print(f"Error creating episode: {response.text}") print(f"Error creating episode: {response.status_code} {response.text}")
sys.exit(1) sys.exit(1)
episode = response.json() episode = response.json()
@@ -312,7 +317,7 @@ def get_next_episode_number() -> int:
headers = get_auth_header() headers = get_auth_header()
response = requests.get( response = requests.get(
f"{CASTOPOD_URL}/api/rest/v1/podcasts/{PODCAST_ID}/episodes", f"{CASTOPOD_URL}/api/rest/v1/episodes",
headers=headers headers=headers
) )
@@ -323,7 +328,12 @@ def get_next_episode_number() -> int:
if not episodes: if not episodes:
return 1 return 1
max_num = max(ep.get("number", 0) for ep in episodes) # Filter to our podcast
our_episodes = [ep for ep in episodes if ep.get("podcast_id") == PODCAST_ID]
if not our_episodes:
return 1
max_num = max(ep.get("number", 0) or 0 for ep in our_episodes)
return max_num + 1 return max_num + 1
@@ -373,7 +383,7 @@ def main():
return return
# Step 3: Create episode # Step 3: Create episode
episode = create_episode(str(audio_path), metadata, transcript["duration"]) episode = create_episode(str(audio_path), metadata, episode_number)
# Step 4: Publish # Step 4: Publish
episode = publish_episode(episode["id"]) episode = publish_episode(episode["id"])

45
run.sh
View File

@@ -1,6 +1,7 @@
#!/bin/bash #!/bin/bash
# AI Radio Show - Server Runner with restart support # AI Radio Show - Server Runner with restart support
PORT=8000
LOG_FILE="/tmp/ai-radio-show.log" LOG_FILE="/tmp/ai-radio-show.log"
RESTART_FLAG="/tmp/ai-radio-show.restart" RESTART_FLAG="/tmp/ai-radio-show.restart"
STOP_FLAG="/tmp/ai-radio-show.stop" STOP_FLAG="/tmp/ai-radio-show.stop"
@@ -13,16 +14,46 @@ source venv/bin/activate
# Cleanup old flags # Cleanup old flags
rm -f "$RESTART_FLAG" "$STOP_FLAG" rm -f "$RESTART_FLAG" "$STOP_FLAG"
# Check if port is already in use
if lsof -i ":$PORT" -sTCP:LISTEN -t >/dev/null 2>&1; then
EXISTING_PID=$(lsof -i ":$PORT" -sTCP:LISTEN -t 2>/dev/null | head -1)
echo "ERROR: Port $PORT is already in use by PID $EXISTING_PID"
echo "Run: kill $EXISTING_PID"
exit 1
fi
kill_server() {
local pid=$1
if ! kill -0 "$pid" 2>/dev/null; then
return
fi
kill "$pid" 2>/dev/null
# Wait up to 5 seconds for graceful shutdown
for i in $(seq 1 10); do
if ! kill -0 "$pid" 2>/dev/null; then
return
fi
sleep 0.5
done
# Force kill if still alive
echo "[$(date)] Server didn't stop gracefully, force killing..." | tee -a "$LOG_FILE"
kill -9 "$pid" 2>/dev/null
wait "$pid" 2>/dev/null
}
echo "AI Radio Show Server Runner" echo "AI Radio Show Server Runner"
echo "Log file: $LOG_FILE" echo "Log file: $LOG_FILE"
echo "Press Ctrl+C to stop" echo "Press Ctrl+C to stop"
echo "" echo ""
while true; do # Handle Ctrl+C
echo "[$(date)] Starting server..." | tee -a "$LOG_FILE" trap 'echo ""; echo "[$(date)] Interrupted" | tee -a "$LOG_FILE"; kill_server $SERVER_PID; exit 0' INT TERM
# Start uvicorn with output to both console and log file while true; do
python -m uvicorn backend.main:app --host 0.0.0.0 --port 8000 2>&1 | tee -a "$LOG_FILE" & echo "[$(date)] Starting server on port $PORT..." | tee -a "$LOG_FILE"
# Start uvicorn directly (not through tee pipe so we get the real PID)
python -m uvicorn backend.main:app --host 0.0.0.0 --port $PORT >> "$LOG_FILE" 2>&1 &
SERVER_PID=$! SERVER_PID=$!
# Wait for server to exit or restart signal # Wait for server to exit or restart signal
@@ -30,8 +61,7 @@ while true; do
if [ -f "$RESTART_FLAG" ]; then if [ -f "$RESTART_FLAG" ]; then
echo "[$(date)] Restart requested..." | tee -a "$LOG_FILE" echo "[$(date)] Restart requested..." | tee -a "$LOG_FILE"
rm -f "$RESTART_FLAG" rm -f "$RESTART_FLAG"
kill $SERVER_PID 2>/dev/null kill_server $SERVER_PID
wait $SERVER_PID 2>/dev/null
sleep 1 sleep 1
break break
fi fi
@@ -39,8 +69,7 @@ while true; do
if [ -f "$STOP_FLAG" ]; then if [ -f "$STOP_FLAG" ]; then
echo "[$(date)] Stop requested..." | tee -a "$LOG_FILE" echo "[$(date)] Stop requested..." | tee -a "$LOG_FILE"
rm -f "$STOP_FLAG" rm -f "$STOP_FLAG"
kill $SERVER_PID 2>/dev/null kill_server $SERVER_PID
wait $SERVER_PID 2>/dev/null
echo "[$(date)] Server stopped." | tee -a "$LOG_FILE" echo "[$(date)] Server stopped." | tee -a "$LOG_FILE"
exit 0 exit 0
fi fi