Standalone Shonar Desktop: vendor portable sources + local engine; decouple from ~/Projects/Shonar
- shared/ = portable Android-origin sources vendored from deferred/desktop-server (app/build.gradle.kts srcDir repointed; PlaybackController.kt excluded as Android-only) - backend/ = bundled-lite engine (SQLite + inline queue); .venv symlinked from the old checkout, PYTHONPATH pins THIS backend's code over any editable install - repoRoot() resolves this project dir (env SHONAR_REPO still wins); desktop-dev.sh watches shared/ + backend/ - Verified: :app:compileKotlin + :app:test green (23 tests); engine boots on :8010, self-migrates, /healthz ok
This commit is contained in:
commit
76c867fca4
136 changed files with 21099 additions and 0 deletions
281
backend/migrations/versions/8d51af959ae4_initial_schema.py
Normal file
281
backend/migrations/versions/8d51af959ae4_initial_schema.py
Normal file
|
|
@ -0,0 +1,281 @@
|
|||
"""initial schema
|
||||
|
||||
Revision ID: 8d51af959ae4
|
||||
Revises:
|
||||
Create Date: 2026-09-08 13:07:10.455837
|
||||
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
revision: str = '8d51af959ae4'
|
||||
down_revision: str | None = None
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def _json_type() -> sa.types.TypeEngine:
|
||||
"""JSONB on PostgreSQL, plain JSON on SQLite (desktop bundled engine)."""
|
||||
if op.get_context().dialect.name == "postgresql":
|
||||
return postgresql.JSONB(astext_type=sa.Text())
|
||||
return sa.JSON()
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('users',
|
||||
sa.Column('email', sa.String(length=320), nullable=False),
|
||||
sa.Column('password_hash', sa.String(length=255), nullable=False),
|
||||
sa.Column('display_name', sa.String(length=120), nullable=True),
|
||||
sa.Column('is_active', sa.Boolean(), nullable=False),
|
||||
sa.Column('location_storage_enabled', sa.Boolean(), nullable=False),
|
||||
sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('id', sa.Uuid(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True)
|
||||
op.create_table('devices',
|
||||
sa.Column('user_id', sa.Uuid(), nullable=False),
|
||||
sa.Column('name', sa.String(length=120), nullable=False),
|
||||
sa.Column('platform', sa.String(length=40), nullable=False),
|
||||
sa.Column('last_seen_at', sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column('revoked_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('id', sa.Uuid(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_devices_user_id'), 'devices', ['user_id'], unique=False)
|
||||
op.create_table('tags',
|
||||
sa.Column('user_id', sa.Uuid(), nullable=False),
|
||||
sa.Column('name', sa.String(length=80), nullable=False),
|
||||
sa.Column('id', sa.Uuid(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('user_id', 'name')
|
||||
)
|
||||
op.create_index(op.f('ix_tags_user_id'), 'tags', ['user_id'], unique=False)
|
||||
op.create_table('recordings',
|
||||
sa.Column('user_id', sa.Uuid(), nullable=False),
|
||||
sa.Column('device_id', sa.Uuid(), nullable=True),
|
||||
sa.Column('client_recording_id', sa.String(length=64), nullable=True),
|
||||
sa.Column('title', sa.String(length=300), nullable=False),
|
||||
sa.Column('recorded_at', sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column('duration_seconds', sa.Float(), nullable=False),
|
||||
sa.Column('notes', sa.Text(), nullable=True),
|
||||
sa.Column('latitude', sa.Float(), nullable=True),
|
||||
sa.Column('longitude', sa.Float(), nullable=True),
|
||||
sa.Column('location_accuracy_m', sa.Float(), nullable=True),
|
||||
sa.Column('processing_status', sa.Enum('pending_upload', 'uploaded', 'queued', 'processing', 'completed', 'failed', 'ai_disabled', name='processing_status'), nullable=False),
|
||||
sa.Column('processing_error', sa.Text(), nullable=True),
|
||||
sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('id', sa.Uuid(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
|
||||
sa.ForeignKeyConstraint(['device_id'], ['devices.id'], ondelete='SET NULL'),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_recordings_client_recording_id'), 'recordings', ['client_recording_id'], unique=False)
|
||||
op.create_index(op.f('ix_recordings_processing_status'), 'recordings', ['processing_status'], unique=False)
|
||||
op.create_index('ix_recordings_user_client_id', 'recordings', ['user_id', 'client_recording_id'], unique=True, postgresql_where='client_recording_id IS NOT NULL', sqlite_where=sa.text('client_recording_id IS NOT NULL'))
|
||||
op.create_index(op.f('ix_recordings_user_id'), 'recordings', ['user_id'], unique=False)
|
||||
op.create_index('ix_recordings_user_recorded', 'recordings', ['user_id', 'recorded_at'], unique=False)
|
||||
op.create_table('refresh_tokens',
|
||||
sa.Column('user_id', sa.Uuid(), nullable=False),
|
||||
sa.Column('token_hash', sa.String(length=128), nullable=False),
|
||||
sa.Column('family', sa.Uuid(), nullable=False),
|
||||
sa.Column('device_id', sa.Uuid(), nullable=True),
|
||||
sa.Column('expires_at', sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column('revoked_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('replaced_by', sa.Uuid(), nullable=True),
|
||||
sa.Column('id', sa.Uuid(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
|
||||
sa.ForeignKeyConstraint(['device_id'], ['devices.id'], ondelete='SET NULL'),
|
||||
sa.ForeignKeyConstraint(['replaced_by'], ['refresh_tokens.id'], ondelete='SET NULL'),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('token_hash')
|
||||
)
|
||||
op.create_index(op.f('ix_refresh_tokens_family'), 'refresh_tokens', ['family'], unique=False)
|
||||
op.create_index(op.f('ix_refresh_tokens_user_id'), 'refresh_tokens', ['user_id'], unique=False)
|
||||
op.create_table('assets',
|
||||
sa.Column('recording_id', sa.Uuid(), nullable=True),
|
||||
sa.Column('user_id', sa.Uuid(), nullable=False),
|
||||
sa.Column('kind', sa.Enum('original', 'normalized', 'export', name='asset_kind'), nullable=False),
|
||||
sa.Column('storage_key', sa.String(length=500), nullable=False),
|
||||
sa.Column('mime_type', sa.String(length=100), nullable=False),
|
||||
sa.Column('size_bytes', sa.BigInteger(), nullable=False),
|
||||
sa.Column('checksum_sha256', sa.String(length=64), nullable=False),
|
||||
sa.Column('id', sa.Uuid(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
|
||||
sa.ForeignKeyConstraint(['recording_id'], ['recordings.id'], ondelete='CASCADE'),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_assets_recording_id'), 'assets', ['recording_id'], unique=False)
|
||||
op.create_index(op.f('ix_assets_user_id'), 'assets', ['user_id'], unique=False)
|
||||
op.create_index('uq_assets_one_original', 'assets', ['recording_id'], unique=True, postgresql_where="kind = 'original'", sqlite_where=sa.text("kind = 'original'"))
|
||||
op.create_table('processing_jobs',
|
||||
sa.Column('recording_id', sa.Uuid(), nullable=False),
|
||||
sa.Column('job_type', sa.Enum('normalize_audio', 'transcribe', 'summarize', name='job_type'), nullable=False),
|
||||
sa.Column('status', sa.Enum('queued', 'running', 'succeeded', 'failed', 'skipped', name='job_status'), nullable=False),
|
||||
sa.Column('attempt', sa.Integer(), nullable=False),
|
||||
sa.Column('max_attempts', sa.Integer(), nullable=False),
|
||||
sa.Column('error', sa.Text(), nullable=True),
|
||||
sa.Column('started_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('finished_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('task_handle', sa.String(length=120), nullable=True),
|
||||
sa.Column('id', sa.Uuid(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
|
||||
sa.ForeignKeyConstraint(['recording_id'], ['recordings.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_processing_jobs_recording_id'), 'processing_jobs', ['recording_id'], unique=False)
|
||||
op.create_index('ix_processing_jobs_recording_type', 'processing_jobs', ['recording_id', 'job_type'], unique=False)
|
||||
op.create_index(op.f('ix_processing_jobs_status'), 'processing_jobs', ['status'], unique=False)
|
||||
op.create_table('recording_tags',
|
||||
sa.Column('recording_id', sa.Uuid(), nullable=False),
|
||||
sa.Column('tag_id', sa.Uuid(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['recording_id'], ['recordings.id'], ondelete='CASCADE'),
|
||||
sa.ForeignKeyConstraint(['tag_id'], ['tags.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('recording_id', 'tag_id')
|
||||
)
|
||||
op.create_table('summaries',
|
||||
sa.Column('recording_id', sa.Uuid(), nullable=False),
|
||||
sa.Column('version', sa.Integer(), nullable=False),
|
||||
sa.Column('superseded_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('provider', sa.String(length=80), nullable=False),
|
||||
sa.Column('model', sa.String(length=120), nullable=True),
|
||||
sa.Column('content', _json_type(), nullable=False),
|
||||
sa.Column('edited_by_user', sa.Boolean(), nullable=False),
|
||||
sa.Column('id', sa.Uuid(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
|
||||
sa.ForeignKeyConstraint(['recording_id'], ['recordings.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_summaries_recording_id'), 'summaries', ['recording_id'], unique=False)
|
||||
op.create_table('transcripts',
|
||||
sa.Column('recording_id', sa.Uuid(), nullable=False),
|
||||
sa.Column('version', sa.Integer(), nullable=False),
|
||||
sa.Column('superseded_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('language', sa.String(length=16), nullable=True),
|
||||
sa.Column('provider', sa.String(length=80), nullable=False),
|
||||
sa.Column('model', sa.String(length=120), nullable=True),
|
||||
sa.Column('text', sa.Text(), nullable=False),
|
||||
sa.Column('segments', _json_type(), nullable=True),
|
||||
sa.Column('edited_by_user', sa.Boolean(), nullable=False),
|
||||
sa.Column('id', sa.Uuid(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
|
||||
sa.ForeignKeyConstraint(['recording_id'], ['recordings.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_transcripts_recording_id'), 'transcripts', ['recording_id'], unique=False)
|
||||
op.create_index('ix_transcripts_recording_version', 'transcripts', ['recording_id', 'version'], unique=False)
|
||||
op.create_table('export_jobs',
|
||||
sa.Column('user_id', sa.Uuid(), nullable=False),
|
||||
sa.Column('recording_id', sa.Uuid(), nullable=False),
|
||||
sa.Column('export_type', sa.String(length=40), nullable=False),
|
||||
sa.Column('status', sa.Enum('queued', 'running', 'succeeded', 'failed', 'skipped', name='export_job_status'), nullable=False),
|
||||
sa.Column('asset_id', sa.Uuid(), nullable=True),
|
||||
sa.Column('error', sa.Text(), nullable=True),
|
||||
sa.Column('id', sa.Uuid(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
|
||||
sa.ForeignKeyConstraint(['asset_id'], ['assets.id'], ondelete='SET NULL'),
|
||||
sa.ForeignKeyConstraint(['recording_id'], ['recordings.id'], ondelete='CASCADE'),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_export_jobs_recording_id'), 'export_jobs', ['recording_id'], unique=False)
|
||||
op.create_index(op.f('ix_export_jobs_user_id'), 'export_jobs', ['user_id'], unique=False)
|
||||
op.create_table('upload_sessions',
|
||||
sa.Column('user_id', sa.Uuid(), nullable=False),
|
||||
sa.Column('recording_id', sa.Uuid(), nullable=True),
|
||||
sa.Column('client_recording_id', sa.String(length=64), nullable=True),
|
||||
sa.Column('title', sa.String(length=300), nullable=True),
|
||||
sa.Column('declared_mime_type', sa.String(length=100), nullable=False),
|
||||
sa.Column('declared_size_bytes', sa.BigInteger(), nullable=False),
|
||||
sa.Column('chunk_size_bytes', sa.BigInteger(), nullable=False),
|
||||
sa.Column('status', sa.Enum('open', 'finalizing', 'completed', 'aborted', 'expired', name='upload_session_status'), nullable=False),
|
||||
sa.Column('expires_at', sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column('completed_asset_id', sa.Uuid(), nullable=True),
|
||||
sa.Column('id', sa.Uuid(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
|
||||
sa.ForeignKeyConstraint(['completed_asset_id'], ['assets.id'], ondelete='SET NULL'),
|
||||
sa.ForeignKeyConstraint(['recording_id'], ['recordings.id'], ondelete='CASCADE'),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_upload_sessions_user_id'), 'upload_sessions', ['user_id'], unique=False)
|
||||
op.create_table('upload_chunks',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('session_id', sa.Uuid(), nullable=False),
|
||||
sa.Column('chunk_index', sa.Integer(), nullable=False),
|
||||
sa.Column('size_bytes', sa.BigInteger(), nullable=False),
|
||||
sa.Column('checksum_sha256', sa.String(length=64), nullable=False),
|
||||
sa.Column('storage_key', sa.String(length=500), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
|
||||
sa.ForeignKeyConstraint(['session_id'], ['upload_sessions.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('session_id', 'chunk_index')
|
||||
)
|
||||
op.create_index(op.f('ix_upload_chunks_session_id'), 'upload_chunks', ['session_id'], unique=False)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index(op.f('ix_upload_chunks_session_id'), table_name='upload_chunks')
|
||||
op.drop_table('upload_chunks')
|
||||
op.drop_index(op.f('ix_upload_sessions_user_id'), table_name='upload_sessions')
|
||||
op.drop_table('upload_sessions')
|
||||
op.drop_index(op.f('ix_export_jobs_user_id'), table_name='export_jobs')
|
||||
op.drop_index(op.f('ix_export_jobs_recording_id'), table_name='export_jobs')
|
||||
op.drop_table('export_jobs')
|
||||
op.drop_index('ix_transcripts_recording_version', table_name='transcripts')
|
||||
op.drop_index(op.f('ix_transcripts_recording_id'), table_name='transcripts')
|
||||
op.drop_table('transcripts')
|
||||
op.drop_index(op.f('ix_summaries_recording_id'), table_name='summaries')
|
||||
op.drop_table('summaries')
|
||||
op.drop_table('recording_tags')
|
||||
op.drop_index(op.f('ix_processing_jobs_status'), table_name='processing_jobs')
|
||||
op.drop_index('ix_processing_jobs_recording_type', table_name='processing_jobs')
|
||||
op.drop_index(op.f('ix_processing_jobs_recording_id'), table_name='processing_jobs')
|
||||
op.drop_table('processing_jobs')
|
||||
op.drop_index('uq_assets_one_original', table_name='assets', postgresql_where="kind = 'original'")
|
||||
op.drop_index(op.f('ix_assets_user_id'), table_name='assets')
|
||||
op.drop_index(op.f('ix_assets_recording_id'), table_name='assets')
|
||||
op.drop_table('assets')
|
||||
op.drop_index(op.f('ix_refresh_tokens_user_id'), table_name='refresh_tokens')
|
||||
op.drop_index(op.f('ix_refresh_tokens_family'), table_name='refresh_tokens')
|
||||
op.drop_table('refresh_tokens')
|
||||
op.drop_index('ix_recordings_user_recorded', table_name='recordings')
|
||||
op.drop_index(op.f('ix_recordings_user_id'), table_name='recordings')
|
||||
op.drop_index('ix_recordings_user_client_id', table_name='recordings', postgresql_where='client_recording_id IS NOT NULL')
|
||||
op.drop_index(op.f('ix_recordings_processing_status'), table_name='recordings')
|
||||
op.drop_index(op.f('ix_recordings_client_recording_id'), table_name='recordings')
|
||||
op.drop_table('recordings')
|
||||
op.drop_index(op.f('ix_tags_user_id'), table_name='tags')
|
||||
op.drop_table('tags')
|
||||
op.drop_index(op.f('ix_devices_user_id'), table_name='devices')
|
||||
op.drop_table('devices')
|
||||
op.drop_index(op.f('ix_users_email'), table_name='users')
|
||||
op.drop_table('users')
|
||||
# ### end Alembic commands ###
|
||||
86
backend/migrations/versions/fts0000000001_fts_columns.py
Normal file
86
backend/migrations/versions/fts0000000001_fts_columns.py
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
"""full-text search columns (PostgreSQL tsvector)
|
||||
|
||||
Revision ID: fts0000000001
|
||||
Revises: 0c938663a363
|
||||
|
||||
Generated (STORED) tsvector columns + GIN indexes for search across title,
|
||||
transcript text, summary content, tags, and action items. The SearchBackend
|
||||
protocol keeps this swappable for Meilisearch/OpenSearch later.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision: str = "fts0000000001"
|
||||
down_revision: str | None = "8d51af959ae4"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# tsvector is PostgreSQL-only. On SQLite (desktop bundled engine) full-
|
||||
# text search is handled outside the DB (the app scans report files),
|
||||
# so this migration is a no-op.
|
||||
if op.get_context().dialect.name != "postgresql":
|
||||
return
|
||||
# recordings: title + notes + tag names are indexed; transcript/summary
|
||||
# contribute via their own tables (joined at query time).
|
||||
op.execute(
|
||||
"""
|
||||
ALTER TABLE recordings
|
||||
ADD COLUMN search_vector tsvector
|
||||
GENERATED ALWAYS AS (
|
||||
setweight(to_tsvector('simple', coalesce(title, '')), 'A') ||
|
||||
setweight(to_tsvector('simple', coalesce(notes, '')), 'B')
|
||||
) STORED;
|
||||
"""
|
||||
)
|
||||
op.execute("CREATE INDEX ix_recordings_search ON recordings USING GIN (search_vector);")
|
||||
|
||||
op.execute(
|
||||
"""
|
||||
ALTER TABLE transcripts
|
||||
ADD COLUMN search_vector tsvector
|
||||
GENERATED ALWAYS AS (
|
||||
setweight(to_tsvector('simple', coalesce(text, '')), 'B')
|
||||
) STORED;
|
||||
"""
|
||||
)
|
||||
op.execute("CREATE INDEX ix_transcripts_search ON transcripts USING GIN (search_vector);")
|
||||
|
||||
# summary content JSON -> extractive text for FTS. Generated columns
|
||||
# forbid subqueries/set-returning functions, so we index the JSON body
|
||||
# with punctuation stripped (covers key_points/decisions/action_items/
|
||||
# questions) plus weighted short/detailed fields.
|
||||
op.execute(
|
||||
"""
|
||||
ALTER TABLE summaries
|
||||
ADD COLUMN search_vector tsvector
|
||||
GENERATED ALWAYS AS (
|
||||
setweight(to_tsvector('simple', coalesce(content->>'short', '')), 'A') ||
|
||||
setweight(to_tsvector('simple', coalesce(content->>'detailed', '')), 'B') ||
|
||||
setweight(
|
||||
to_tsvector('simple',
|
||||
regexp_replace(coalesce(content::text, ''), '[\\[\\]{}"]', ' ', 'g')), 'C')
|
||||
) STORED;
|
||||
"""
|
||||
)
|
||||
op.execute("CREATE INDEX ix_summaries_search ON summaries USING GIN (search_vector);")
|
||||
|
||||
op.execute(
|
||||
"""
|
||||
ALTER TABLE tags ADD COLUMN IF NOT EXISTS search_vector tsvector
|
||||
GENERATED ALWAYS AS (to_tsvector('simple', coalesce(name, ''))) STORED;
|
||||
"""
|
||||
)
|
||||
op.execute("CREATE INDEX ix_tags_search ON tags USING GIN (search_vector);")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
if op.get_context().dialect.name != "postgresql":
|
||||
return
|
||||
for table in ("tags", "summaries", "transcripts", "recordings"):
|
||||
op.execute(f"DROP INDEX IF EXISTS ix_{table}_search;")
|
||||
op.execute(f"ALTER TABLE {table} DROP COLUMN IF EXISTS search_vector;")
|
||||
69
backend/migrations/versions/m8models000001_model_support.py
Normal file
69
backend/migrations/versions/m8models000001_model_support.py
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
"""per-recording transcription model + job progress + app settings
|
||||
|
||||
Revision ID: m8models000001
|
||||
Revises: fts0000000001
|
||||
|
||||
- recordings.transcription_model (nullable; NULL = server default)
|
||||
- processing_jobs.stage / processing_jobs.progress (display-only)
|
||||
- app_settings table (global default model, …)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
revision: str = 'm8models000001'
|
||||
down_revision: str | None = 'fts0000000001'
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def _json_type() -> sa.types.TypeEngine:
|
||||
"""JSONB on PostgreSQL, plain JSON on SQLite (desktop bundled engine)."""
|
||||
if op.get_context().dialect.name == "postgresql":
|
||||
return postgresql.JSONB(astext_type=sa.Text())
|
||||
return sa.JSON()
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column(
|
||||
"recordings",
|
||||
sa.Column("transcription_model", sa.String(length=32), nullable=True),
|
||||
)
|
||||
op.add_column(
|
||||
"upload_sessions",
|
||||
sa.Column("transcription_model", sa.String(length=32), nullable=True),
|
||||
)
|
||||
op.add_column(
|
||||
"processing_jobs",
|
||||
sa.Column("stage", sa.String(length=32), nullable=True),
|
||||
)
|
||||
op.add_column(
|
||||
"processing_jobs",
|
||||
sa.Column("progress", sa.Integer(), nullable=True),
|
||||
)
|
||||
op.create_table(
|
||||
"app_settings",
|
||||
sa.Column("key", sa.String(length=120), nullable=False),
|
||||
sa.Column(
|
||||
"value",
|
||||
_json_type(),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("key"),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_table("app_settings")
|
||||
op.drop_column("processing_jobs", "progress")
|
||||
op.drop_column("processing_jobs", "stage")
|
||||
op.drop_column("upload_sessions", "transcription_model")
|
||||
op.drop_column("recordings", "transcription_model")
|
||||
Loading…
Add table
Add a link
Reference in a new issue