NoteStack-Test / backend /alembic /versions /0009_b2v_resources.py
Muhammad-Mehdi's picture
wired blog2video api phase 1
070c931
Raw History Blame Contribute Delete
6.06 kB
"""blog2video resources per workspace: templates, custom voices, saved voices, styles, usage counters;
b2v_videos.created_via / template_ref; plans.video_premium / video_limits
Everything made with our blog2video key belongs to one account, so these tables are what keeps each workspace's
templates, voices and styles its own (app/services/b2v_access.py).
Revision ID: 0009_b2v_resources
Revises: 0008_b2v_videos
Create Date: 2026-09-30
"""
import json
import sqlalchemy as sa
from alembic import op
revision = "0009_b2v_resources"
down_revision = "0008_b2v_videos"
branch_labels = None
depends_on = None
# Same as app/services/plans.py
VIDEO_LIMITS = {
"free": {"ai_edits": 20, "templates": 0, "template_ai_daily": 0, "voice_designs_daily": 0,
"voice_samples_daily": 0, "custom_voices": 0},
"writer": {"ai_edits": 300, "templates": 2, "template_ai_daily": 3, "voice_designs_daily": 5,
"voice_samples_daily": 20, "custom_voices": 3},
"studio": {"ai_edits": 1000, "templates": 5, "template_ai_daily": 5, "voice_designs_daily": 10,
"voice_samples_daily": 40, "custom_voices": 10},
}
def _ws() -> sa.Column:
return sa.Column("workspace_id", sa.Uuid(), sa.ForeignKey("workspaces.id", ondelete="CASCADE"), nullable=False,
index=True)
def _times() -> list[sa.Column]:
return [sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now())]
def upgrade() -> None:
bind = op.get_bind()
inspector = sa.inspect(bind)
# 0001 builds from the live models, so a fresh database may already have all of this.
video_cols = {c["name"] for c in inspector.get_columns("b2v_videos")}
with op.batch_alter_table("b2v_videos") as batch:
if "created_via" not in video_cols:
batch.add_column(sa.Column("created_via", sa.String(10), nullable=False, server_default="v1"))
if "template_ref" not in video_cols:
batch.add_column(sa.Column("template_ref", sa.String(100), nullable=True))
batch.alter_column("idempotency_key", existing_type=sa.String(100), nullable=True)
if not inspector.has_table("b2v_templates"):
op.create_table("b2v_templates",
sa.Column("b2v_template_id", sa.Integer(), primary_key=True, autoincrement=False), _ws(),
sa.Column("name", sa.String(255), nullable=False),
sa.Column("ready", sa.Boolean(), nullable=False, server_default=sa.false()), *_times())
if not inspector.has_table("b2v_custom_voices"):
op.create_table("b2v_custom_voices",
sa.Column("b2v_custom_voice_id", sa.Integer(), primary_key=True, autoincrement=False), _ws(),
sa.Column("voice_id", sa.String(100), nullable=False, unique=True),
sa.Column("name", sa.String(255), nullable=False),
sa.Column("source", sa.String(20), nullable=False),
sa.Column("preview_url", sa.String(1000), nullable=True), *_times())
if not inspector.has_table("user_saved_voices"):
op.create_table("user_saved_voices",
sa.Column("workspace_id", sa.Uuid(), sa.ForeignKey("workspaces.id", ondelete="CASCADE"),
primary_key=True),
sa.Column("voice_id", sa.String(100), primary_key=True),
sa.Column("name", sa.String(255), nullable=False),
sa.Column("preview_url", sa.String(1000), nullable=True),
sa.Column("gender", sa.String(20), nullable=True),
sa.Column("accent", sa.String(50), nullable=True),
sa.Column("premium", sa.Boolean(), nullable=False, server_default=sa.false()),
sa.Column("is_custom", sa.Boolean(), nullable=False, server_default=sa.false()), *_times())
if not inspector.has_table("b2v_styles"):
op.create_table("b2v_styles",
sa.Column("b2v_style_id", sa.Integer(), primary_key=True, autoincrement=False), _ws(),
sa.Column("name", sa.String(80), nullable=False), *_times())
if not inspector.has_table("usage_counters"):
op.create_table("usage_counters",
sa.Column("workspace_id", sa.Uuid(), sa.ForeignKey("workspaces.id", ondelete="CASCADE"),
primary_key=True),
sa.Column("period", sa.String(10), primary_key=True),
sa.Column("metric", sa.String(30), primary_key=True),
sa.Column("used", sa.Integer(), nullable=False, server_default="0"))
plan_cols = {c["name"] for c in inspector.get_columns("plans")}
with op.batch_alter_table("plans") as batch:
if "video_premium" not in plan_cols:
batch.add_column(sa.Column("video_premium", sa.Boolean(), nullable=False, server_default=sa.false()))
if "video_limits" not in plan_cols:
batch.add_column(sa.Column("video_limits", sa.JSON(), nullable=True))
plans = sa.table("plans", sa.column("id", sa.String), sa.column("video_premium", sa.Boolean),
sa.column("video_limits", sa.JSON))
for plan_id, limits in VIDEO_LIMITS.items():
op.execute(plans.update().where(plans.c.id == plan_id)
.values(video_premium=plan_id != "free", video_limits=json.loads(json.dumps(limits))))
def downgrade() -> None:
with op.batch_alter_table("plans") as batch:
batch.drop_column("video_limits")
batch.drop_column("video_premium")
for table in ("usage_counters", "b2v_styles", "user_saved_voices", "b2v_custom_voices", "b2v_templates"):
op.drop_table(table)
with op.batch_alter_table("b2v_videos") as batch:
batch.drop_column("template_ref")
batch.drop_column("created_via")