Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,14 @@ result-*
*.sqlite3
db.sqlite3-journal

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

# --- 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