stage3: второй канал — подкаст (audio asset) + idempotent публикации

This commit is contained in:
neo committed 2026-09-14 14:53:45 +05:00
1 parent c57d7666e6
commit c417ddc48f
1 file changed
+206 -22
+206 -22
View File
@@ -172,23 +172,30 @@ def create_publication(conn, project_id: str, project_slug: str) -> dict:
# Проверить, есть ли уже публикация (идемпотентность) # Проверить, есть ли уже публикация (идемпотентность)
cur.execute( cur.execute(
"SELECT 1 FROM publications WHERE composition_id = %s LIMIT 1", "SELECT id FROM publications WHERE composition_id = %s AND target LIKE %s LIMIT 1",
(comp_id,), (comp_id, "%video%"),
) )
if cur.fetchone(): row = cur.fetchone()
return { if row:
pub_id = row[0]
cur.execute(
"""
UPDATE publications SET metrics = %s WHERE id = %s
""",
(
json.dumps(
{
"views": 1200, "views": 1200,
"ctr": 0.045, "ctr": 0.045,
"clicks": 54, "clicks": 54,
"conversions": 4, "conversions": 4,
"revenue": 600.0, "revenue": 600.0,
} }
views = 1200 ),
ctr = 0.045 pub_id,
clicks = int(views * ctr) ),
conv = int(clicks * 0.08) )
revenue = round(conv * 150.0, 2) # $150 / conversion else:
cur.execute( cur.execute(
""" """
INSERT INTO publications INSERT INTO publications
@@ -201,11 +208,11 @@ def create_publication(conn, project_id: str, project_slug: str) -> dict:
"vid-" + project_slug, "vid-" + project_slug,
json.dumps( json.dumps(
{ {
"views": views, "views": 1200,
"ctr": ctr, "ctr": 0.045,
"clicks": clicks, "clicks": 54,
"conversions": conv, "conversions": 4,
"revenue": revenue, "revenue": 600.0,
} }
), ),
), ),
@@ -213,10 +220,184 @@ def create_publication(conn, project_id: str, project_slug: str) -> dict:
conn.commit() conn.commit()
return { return {
"views": views, "views": 1200,
"clicks": clicks, "clicks": 54,
"conversions": conv, "conversions": 4,
"revenue": revenue, "revenue": 600.0,
}
def create_podcast_asset(conn, project_id: str, project_slug: str, atoms: list[dict]) -> dict | None:
"""Создаёт audio asset (подкаст) + atom_assets (таймкоды) + podcast composition."""
if not atoms:
return None
asset_id = None
total_sec = 0
# Чистим старые audio-активы для проекта (идемпотентность)
with conn.cursor() as cur:
cur.execute(
"DELETE FROM atom_assets WHERE asset_id IN (SELECT id FROM assets WHERE project_id = %s AND asset_type = 'audio')",
(project_id,),
)
cur.execute("DELETE FROM assets WHERE project_id = %s AND asset_type = 'audio'", (project_id,))
conn.commit()
# Длительность эпизода — ~10 сек на атом (речевой формат)
for atom in atoms:
total_sec += 10
# Запись audio актива (подкаст)
with conn.cursor() as cur:
cur.execute(
"""
INSERT INTO assets
(project_id, asset_type, format, uri, title, duration_sec, meta)
SELECT %s, 'audio', 'mp3', %s, %s, %s, %s
WHERE NOT EXISTS (
SELECT 1 FROM assets WHERE project_id = %s AND asset_type = 'audio'
)
RETURNING id
""",
(
project_id,
"https://cdn.content-atom.local/" + project_slug + "-podcast.mp3",
f"{project_slug} — подкаст (media stage 3)",
total_sec,
json.dumps({"channel": "podcast", "atoms": len(atoms)}),
project_id,
),
)
row = cur.fetchone()
if row:
asset_id = str(row[0])
conn.commit()
# Привязываем атомы к эпизоду по таймкодам
cursor = 0
for atom in atoms:
start = cursor
dur = 10
end = start + dur
cursor = end
with conn.cursor() as cur:
cur.execute(
"""
INSERT INTO atom_assets (atom_id, asset_id, start_time_sec, end_time_sec, role)
VALUES (%s, %s, %s, %s, 'narration')
ON CONFLICT (atom_id, asset_id, start_time_sec) DO NOTHING
""",
(atom["id"], asset_id, start, end),
)
conn.commit()
# composition: podcast
atom_ids = [a["id"] for a in atoms]
recipe = {
"channel": "podcast",
"format": "audio",
"per_atom_seconds": 10,
"atoms": [{"id": a["id"], "type": a["type"]} for a in atoms],
}
with conn.cursor() as cur:
cur.execute(
"""
INSERT INTO compositions
(project_id, channel, slug, title, status, recipe, atom_ids)
VALUES (%s, 'podcast', %s, %s, 'ready', %s, %s::uuid[])
ON CONFLICT (project_id, channel, slug) DO UPDATE
SET recipe = EXCLUDED.recipe, atom_ids = EXCLUDED.atom_ids, status = 'ready'
""",
(
project_id,
project_slug + "-podcast",
f"{project_slug} — подкаст (media stage 3)",
json.dumps(recipe),
atom_ids,
),
)
conn.commit()
return {"asset": asset_id, "duration_sec": total_sec, "atoms": len(atoms)}
def create_podcast_publication(conn, project_id: str, project_slug: str) -> dict:
"""Создаёт публикацию подкаста (RSS/feed) с метриками."""
with conn.cursor() as cur:
cur.execute(
"""
INSERT INTO compositions
(project_id, channel, slug, title, status, recipe, atom_ids)
VALUES (%s, 'podcast', %s, %s, 'published', '{}', '{}')
ON CONFLICT (project_id, channel, slug) DO UPDATE
SET status = 'published'
RETURNING id
""",
(
project_id,
project_slug + "-podcast",
f"{project_slug} — подкаст",
),
)
comp_id = cur.fetchone()[0]
# Проверить, есть ли уже публикация (идемпотентность)
cur.execute(
"SELECT id FROM publications WHERE composition_id = %s AND target LIKE %s LIMIT 1",
(comp_id, "%podcast%"),
)
row = cur.fetchone()
if row:
pub_id = row[0]
cur.execute(
"""
UPDATE publications SET metrics = %s WHERE id = %s
""",
(
json.dumps(
{
"downloads": 320,
"subscribers": 24,
"listen_rate": 0.78,
"conversions": 3,
"revenue": 450.0,
}
),
pub_id,
),
)
else:
cur.execute(
"""
INSERT INTO publications
(composition_id, target, external_id, published_at, metrics)
VALUES (%s, %s, %s, now(), %s)
""",
(
comp_id,
"https://feeds.content-atom.local/" + project_slug + "-podcast",
"pod-" + project_slug,
json.dumps(
{
"downloads": 320,
"subscribers": 24,
"listen_rate": 0.78,
"conversions": 3,
"revenue": 450.0,
}
),
),
)
conn.commit()
return {
"downloads": 320,
"subscribers": 24,
"listen_rate": 0.78,
"conversions": 3,
"revenue": 450.0,
} }
@@ -237,14 +418,17 @@ def main():
atoms = get_media_atoms(conn, proj["id"]) atoms = get_media_atoms(conn, proj["id"])
asset = create_video_asset(conn, proj["id"], proj["slug"], atoms) asset = create_video_asset(conn, proj["id"], proj["slug"], atoms)
pub = create_publication(conn, proj["id"], proj["slug"]) pub = create_publication(conn, proj["id"], proj["slug"])
pod_asset = create_podcast_asset(conn, proj["id"], proj["slug"], atoms)
pod_pub = create_podcast_publication(conn, proj["id"], proj["slug"])
print( print(
f" • {proj['slug']}: {asset['atoms']} атомов, " f" • {proj['slug']}: {asset['atoms']} атомов, "
f"{asset['duration_sec']:.0f}с видео, " f"{asset['duration_sec']:.0f}с видео, {pod_asset['duration_sec']:.0f}с подкаст, "
f"{pub['views']} views → {pub['conversions']} conv, " f"{pub['views']} views → {pub['conversions']} conv, "
f"${pub['revenue']}" f"{pod_pub['downloads']} downloads подкаст, "
f"${pub['revenue'] + pod_pub['revenue']}"
) )
total_revenue += pub["revenue"] total_revenue += pub["revenue"] + pod_pub["revenue"]
conn.close() conn.close()
print( print(