Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .github/workflows/publish-nightly.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
name: Publish Nightly to PyPI

on:
push:
branches:
- dev
workflow_dispatch:

permissions:
Expand Down
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,17 @@ result-*
*.sqlite3
db.sqlite3-journal

# --- RadioShaq local data ---
radioshaq/scripts/demo/recordings/
radioshaq/config.yaml

# --- RadioShaq web UI build output ---
radioshaq/radioshaq/web_ui/assets/

# --- RadioShaq local env / tools ---
radioshaq/.venv-wsl/
radioshaq/hackrf/

# --- Optional: uncomment if you don’t want lockfiles in vcs ---
# uv.lock
# package-lock.json
Expand Down
5 changes: 4 additions & 1 deletion radioshaq/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ POSTGRES_PASSWORD=radioshaq
# RADIOSHAQ_DATABASE__DYNAMODB_ENDPOINT=
# RADIOSHAQ_DATABASE__DYNAMODB_REGION=us-east-1
# RADIOSHAQ_DATABASE__REDIS_URL=redis://localhost:6379/0
# RADIOSHAQ_DATABASE__ALEMBIC_CONFIG=infrastructure/local/alembic.ini
# RADIOSHAQ_DATABASE__ALEMBIC_CONFIG=alembic.ini
# RADIOSHAQ_DATABASE__AUTO_MIGRATE=false

# -----------------------------------------------------------------------------
Expand Down Expand Up @@ -146,6 +146,9 @@ POSTGRES_PASSWORD=radioshaq
# RADIOSHAQ_AUDIO__HIGHPASS_CUTOFF_HZ=80.0
# RADIOSHAQ_AUDIO__DENOISING_ENABLED=true
# RADIOSHAQ_AUDIO__DENOISING_BACKEND=rnnoise
# When true and ASR model is 'scribe', run ElevenLabs Voice Isolator (audio-isolation)
# before Scribe STT. Requires ELEVENLABS_API_KEY.
# RADIOSHAQ_AUDIO__ELEVEN_VOICE_ISOLATOR_ENABLED=false
# RADIOSHAQ_AUDIO__NOISE_CALIBRATION_SECONDS=3.0
# RADIOSHAQ_AUDIO__MIN_SNR_DB=3.0
# RADIOSHAQ_AUDIO__VAD_ENABLED=true
Expand Down
103 changes: 0 additions & 103 deletions radioshaq/PR_DESCRIPTION.md

This file was deleted.

35 changes: 26 additions & 9 deletions radioshaq/alembic/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,27 +36,44 @@

def get_database_url() -> str:
"""Get database URL from environment or config.

Returns:
PostgreSQL connection URL for migrations (sync)
PostgreSQL connection URL for migrations (sync).
Uses psycopg2, adds connect_timeout and optional sslmode=disable
so migrations do not hang (e.g. on SSL handshake or slow network).
"""
# Query params: avoid hang on connect (timeout) and optional no-SSL (WSL/Docker)
connect_timeout = os.getenv("ALEMBIC_CONNECT_TIMEOUT", "10")
extra_params = f"connect_timeout={connect_timeout}"
if os.getenv("ALEMBIC_SSLMODE_DISABLE", "").lower() in ("1", "true", "yes"):
extra_params = f"sslmode=disable&{extra_params}"

# Priority: DATABASE_URL > individual vars > default
if database_url := os.getenv("DATABASE_URL"):
# Convert async URL to sync URL if needed
if "+asyncpg" in database_url:
return database_url.replace("+asyncpg", "")
database_url = database_url.replace("+asyncpg", "")
if "+aiosqlite" in database_url:
return database_url.replace("+aiosqlite", "")
database_url = database_url.replace("+aiosqlite", "")
# Ensure sync driver for migrations (psycopg2)
if "postgresql://" in database_url and "+" not in database_url.split("//")[0]:
database_url = database_url.replace("postgresql://", "postgresql+psycopg2://", 1)
# Append timeout (and optional sslmode) if not already present
base, _, query = database_url.partition("?")
if "connect_timeout" not in query:
query = f"{query}&{extra_params}" if query else extra_params
database_url = f"{base}?{query.lstrip('&')}"
return database_url
# Build from individual components

# Build from individual components (default port 5434 to match local Docker Postgres)
host = os.getenv("POSTGRES_HOST", "localhost")
port = os.getenv("POSTGRES_PORT", "5432")
port = os.getenv("POSTGRES_PORT", "5434")
database = os.getenv("POSTGRES_DB", "radioshaq")
user = os.getenv("POSTGRES_USER", "radioshaq")
password = os.getenv("POSTGRES_PASSWORD", "radioshaq")

return f"postgresql://{user}:{password}@{host}:{port}/{database}"

url = f"postgresql+psycopg2://{user}:{password}@{host}:{port}/{database}?{extra_params}"
return url


def run_migrations_offline() -> None:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""registered_callsigns preferred_bands and last_band

Revision ID: c3d4e5f6a7b8
Revises: b2c3d4e5f6a7
Create Date: 2026-03-04 11:00:00.000000

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa

revision: str = "c3d4e5f6a7b8"
down_revision: Union[str, None] = "b2c3d4e5f6a7"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
op.add_column(
"registered_callsigns",
sa.Column("preferred_bands", sa.JSON(), nullable=True),
)
op.add_column(
"registered_callsigns",
sa.Column("last_band", sa.String(length=20), nullable=True),
)


def downgrade() -> None:
op.drop_column("registered_callsigns", "last_band")
op.drop_column("registered_callsigns", "preferred_bands")

Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"""registered_callsigns contact preferences (notify-on-relay Section 8.1)

Revision ID: d4e5f6a7b8c9
Revises: c3d4e5f6a7b8
Create Date: 2026-03-07 00:00:00.000000

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa

revision: str = "d4e5f6a7b8c9"
down_revision: Union[str, None] = "c3d4e5f6a7b8"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
op.add_column(
"registered_callsigns",
sa.Column("notify_sms_phone", sa.String(length=20), nullable=True),
)
op.add_column(
"registered_callsigns",
sa.Column("notify_whatsapp_phone", sa.String(length=20), nullable=True),
)
op.add_column(
"registered_callsigns",
sa.Column("notify_on_relay", sa.Boolean(), nullable=False, server_default=sa.false()),
)
op.add_column(
"registered_callsigns",
sa.Column("notify_consent_at", sa.DateTime(timezone=True), nullable=True),
)
op.add_column(
"registered_callsigns",
sa.Column("notify_consent_source", sa.String(length=20), nullable=True),
)
op.add_column(
"registered_callsigns",
sa.Column("notify_opt_out_at", sa.DateTime(timezone=True), nullable=True),
)


def downgrade() -> None:
op.drop_column("registered_callsigns", "notify_opt_out_at")
op.drop_column("registered_callsigns", "notify_consent_source")
op.drop_column("registered_callsigns", "notify_consent_at")
op.drop_column("registered_callsigns", "notify_on_relay")
op.drop_column("registered_callsigns", "notify_whatsapp_phone")
op.drop_column("registered_callsigns", "notify_sms_phone")

Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""registered_callsigns per-channel opt-out (notify_opt_out_at_sms, notify_opt_out_at_whatsapp)

Revision ID: e5f6a7b8c9d0
Revises: d4e5f6a7b8c9
Create Date: 2026-03-07 10:00:00.000000

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa

revision: str = "e5f6a7b8c9d0"
down_revision: Union[str, None] = "d4e5f6a7b8c9"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
op.add_column(
"registered_callsigns",
sa.Column("notify_opt_out_at_sms", sa.DateTime(timezone=True), nullable=True),
)
op.add_column(
"registered_callsigns",
sa.Column("notify_opt_out_at_whatsapp", sa.DateTime(timezone=True), nullable=True),
)


def downgrade() -> None:
op.drop_column("registered_callsigns", "notify_opt_out_at_whatsapp")
op.drop_column("registered_callsigns", "notify_opt_out_at_sms")

3 changes: 2 additions & 1 deletion radioshaq/config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ database:
dynamodb_endpoint: null # e.g. http://localhost:4566 for localstack
dynamodb_region: us-east-1
redis_url: "redis://localhost:6379/0"
alembic_config: "infrastructure/local/alembic.ini"
alembic_config: "alembic.ini"
auto_migrate: false

# -----------------------------------------------------------------------------
Expand Down Expand Up @@ -155,6 +155,7 @@ audio:
denoising_backend: rnnoise # rnnoise | spectral | none
noise_calibration_seconds: 3.0
min_snr_db: 3.0
eleven_voice_isolator_enabled: false # When true and asr_model is 'scribe', run ElevenLabs Voice Isolator before Scribe STT (requires ELEVENLABS_API_KEY).
vad_enabled: true
vad_threshold: 0.02
vad_mode: aggressive # normal | low | aggressive | very_aggressive
Expand Down
19 changes: 19 additions & 0 deletions radioshaq/examples/config_sample.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,25 @@ radio:
listener_concurrent_bands: true # false = single receiver, round-robin
receiver_upload_store: false
receiver_upload_inject: false
# SDR TX (HackRF) coordination
# When enabled, RadioShaq can transmit via HackRF either directly from HQ (local)
# or via a remote receiver service (broker).
sdr_tx_enabled: false
sdr_tx_backend: hackrf
# sdr_tx_mode: local # HQ owns HackRF directly (pyhackrf2); do not also run run-receiver with HackRF.
# sdr_tx_mode: remote # Remote receiver owns HackRF; HQ calls /tx endpoints on the receiver service.
# sdr_tx_service_base_url: "http://localhost:8765" # Required when sdr_tx_mode=remote
# sdr_tx_service_token: "<SERVICE_BEARER_TOKEN>" # Required when remote receiver enforces JWT on /tx/* endpoints.

twilio:
# Twilio configuration for SMS/WhatsApp relay.
# In development you can set allow_unsigned_webhooks=true to accept unsigned webhooks,
# but in production you must configure auth_token and rely on signature validation.
account_sid: null
auth_token: null
from_number: null
whatsapp_from: null
allow_unsigned_webhooks: false
cat_enabled: false
audio_input_enabled: false
audio_output_enabled: false
Expand Down
2 changes: 1 addition & 1 deletion radioshaq/infrastructure/aws/lambda/message_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def _forward_to_hq(payload: dict[str, Any]) -> bool:
with urllib.request.urlopen(req, timeout=10) as resp:
return 200 <= resp.status < 300
except Exception as e:
logger.warning("HQ forward failed: %s", e)
logger.warning("HQ forward failed: {}", e)
return False


Expand Down
4 changes: 2 additions & 2 deletions radioshaq/infrastructure/local/alembic.ini
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
# Database migration management

[alembic]
# Path to migration scripts
script_location = infrastructure/local/alembic
# Path to migration scripts (use root Alembic tree)
script_location = alembic

# Template used to generate migration files
file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
Expand Down
Loading