From c417ddc48fb03daad5d44b338f2ebe290832620d Mon Sep 17 00:00:00 2001 From: neo Date: Mon, 14 Sep 2026 14:53:45 +0500 Subject: [PATCH] =?UTF-8?q?stage3:=20=D0=B2=D1=82=D0=BE=D1=80=D0=BE=D0=B9?= =?UTF-8?q?=20=D0=BA=D0=B0=D0=BD=D0=B0=D0=BB=20=E2=80=94=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=B4=D0=BA=D0=B0=D1=81=D1=82=20(audio=20asset)=20+=20idempote?= =?UTF-8?q?nt=20=D0=BF=D1=83=D0=B1=D0=BB=D0=B8=D0=BA=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- stage3/media_assets.py | 258 +++++++++++++++++++++++++++++++++++------ 1 file changed, 221 insertions(+), 37 deletions(-) diff --git a/stage3/media_assets.py b/stage3/media_assets.py index f1de003..f7b9cff 100644 --- a/stage3/media_assets.py +++ b/stage3/media_assets.py @@ -172,51 +172,232 @@ def create_publication(conn, project_id: str, project_slug: str) -> dict: # Проверить, есть ли уже публикация (идемпотентность) cur.execute( - "SELECT 1 FROM publications WHERE composition_id = %s LIMIT 1", - (comp_id,), + "SELECT id FROM publications WHERE composition_id = %s AND target LIKE %s LIMIT 1", + (comp_id, "%video%"), ) - if cur.fetchone(): - return { - "views": 1200, - "ctr": 0.045, - "clicks": 54, - "conversions": 4, - "revenue": 600.0, - } - views = 1200 - ctr = 0.045 - clicks = int(views * ctr) - conv = int(clicks * 0.08) - revenue = round(conv * 150.0, 2) # $150 / conversion + row = cur.fetchone() + if row: + pub_id = row[0] + cur.execute( + """ + UPDATE publications SET metrics = %s WHERE id = %s + """, + ( + json.dumps( + { + "views": 1200, + "ctr": 0.045, + "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( """ - INSERT INTO publications - (composition_id, target, external_id, published_at, metrics) - VALUES (%s, %s, %s, now(), %s) + 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 """, ( - comp_id, - "youtube.com/watch?v=" + project_slug + "-video", - "vid-" + project_slug, - json.dumps( - { - "views": views, - "ctr": ctr, - "clicks": clicks, - "conversions": conv, - "revenue": revenue, - } - ), + 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 { - "views": views, - "clicks": clicks, - "conversions": conv, - "revenue": revenue, + "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"]) asset = create_video_asset(conn, proj["id"], proj["slug"], atoms) 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( 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['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() print(