stage3: второй канал — подкаст (audio asset) + idempotent публикации
This commit is contained in:
1 parent
c57d7666e6
commit
c417ddc48f
1 file changed
+221
-37
+221
-37
@@ -172,51 +172,232 @@ 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:
|
||||||
"views": 1200,
|
pub_id = row[0]
|
||||||
"ctr": 0.045,
|
cur.execute(
|
||||||
"clicks": 54,
|
"""
|
||||||
"conversions": 4,
|
UPDATE publications SET metrics = %s WHERE id = %s
|
||||||
"revenue": 600.0,
|
""",
|
||||||
}
|
(
|
||||||
views = 1200
|
json.dumps(
|
||||||
ctr = 0.045
|
{
|
||||||
clicks = int(views * ctr)
|
"views": 1200,
|
||||||
conv = int(clicks * 0.08)
|
"ctr": 0.045,
|
||||||
revenue = round(conv * 150.0, 2) # $150 / conversion
|
"clicks": 54,
|
||||||
|
"conversions": 4,
|
||||||
|
"revenue": 600.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,
|
||||||
|
"youtube.com/watch?v=" + project_slug + "-video",
|
||||||
|
"vid-" + project_slug,
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"views": 1200,
|
||||||
|
"ctr": 0.045,
|
||||||
|
"clicks": 54,
|
||||||
|
"conversions": 4,
|
||||||
|
"revenue": 600.0,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"views": 1200,
|
||||||
|
"clicks": 54,
|
||||||
|
"conversions": 4,
|
||||||
|
"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(
|
cur.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO publications
|
INSERT INTO assets
|
||||||
(composition_id, target, external_id, published_at, metrics)
|
(project_id, asset_type, format, uri, title, duration_sec, meta)
|
||||||
VALUES (%s, %s, %s, now(), %s)
|
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
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
comp_id,
|
project_id,
|
||||||
"youtube.com/watch?v=" + project_slug + "-video",
|
"https://cdn.content-atom.local/" + project_slug + "-podcast.mp3",
|
||||||
"vid-" + project_slug,
|
f"{project_slug} — подкаст (media stage 3)",
|
||||||
json.dumps(
|
total_sec,
|
||||||
{
|
json.dumps({"channel": "podcast", "atoms": len(atoms)}),
|
||||||
"views": views,
|
project_id,
|
||||||
"ctr": ctr,
|
),
|
||||||
"clicks": clicks,
|
)
|
||||||
"conversions": conv,
|
row = cur.fetchone()
|
||||||
"revenue": revenue,
|
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()
|
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 {
|
return {
|
||||||
"views": views,
|
"downloads": 320,
|
||||||
"clicks": clicks,
|
"subscribers": 24,
|
||||||
"conversions": conv,
|
"listen_rate": 0.78,
|
||||||
"revenue": revenue,
|
"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(
|
||||||
|
|||||||
Reference in new issue
Block a user