128 lines
4.0 KiB
Python
128 lines
4.0 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
MVP stage 2: compositions — собрать atom_versions под второй канал (Telegram).
|
||
|
||
Для каждого атома генерирует версию под Telegram (plain text, до 1024 символов),
|
||
если atom_versions для этого канала ещё нет.
|
||
|
||
bge-m3 (dim=1024) — единственная embed-модель (для semantic-версий).
|
||
Postgres (atom-db) — atom_versions как источник.
|
||
|
||
Запуск:
|
||
python3 stage2/compositions.py --channel telegram --top 20
|
||
python3 stage2/compositions.py --channel telegram --limit 50
|
||
"""
|
||
|
||
import argparse
|
||
import json
|
||
import os
|
||
import sys
|
||
import time
|
||
|
||
import psycopg2
|
||
|
||
POSTGRES = {
|
||
"host": os.environ.get("POSTGRES_HOST", "127.0.0.1"),
|
||
"port": int(os.environ.get("POSTGRES_PORT", "5434")),
|
||
"user": os.environ.get("POSTGRES_USER", "atom"),
|
||
"dbname": os.environ.get("POSTGRES_DB", "content_atom"),
|
||
"password": os.environ.get("POSTGRES_PASSWORD", "atompass123"),
|
||
}
|
||
MAX_LEN = 1024 # Telegram limit
|
||
|
||
|
||
def get_conn():
|
||
return psycopg2.connect(**POSTGRES)
|
||
|
||
|
||
def get_atoms(conn, limit: int) -> list[dict]:
|
||
"""Атомы для генерации (draft, не занятые)."""
|
||
with conn.cursor() as cur:
|
||
cur.execute("""
|
||
SELECT a.id, a.type, a.plain_text, a.word_count, a.project_id
|
||
FROM atoms a
|
||
WHERE a.plain_text IS NOT NULL
|
||
ORDER BY a.created_at DESC
|
||
LIMIT %s
|
||
""", (limit,))
|
||
cols = [d[0] for d in cur.description]
|
||
return [dict(zip(cols, row)) for row in cur.fetchall()]
|
||
|
||
|
||
def has_version(conn, atom_id: str, channel: str) -> bool:
|
||
with conn.cursor() as cur:
|
||
cur.execute(
|
||
"SELECT 1 FROM atom_versions WHERE atom_id=%s AND channel=%s LIMIT 1",
|
||
(atom_id, channel),
|
||
)
|
||
return cur.fetchone() is not None
|
||
|
||
|
||
def build_telegram_version(atom: dict) -> str:
|
||
"""Генерация версии под Telegram (plain text)."""
|
||
text = atom["plain_text"]
|
||
if len(text) > MAX_LEN:
|
||
text = text[:MAX_LEN].rstrip() + "…"
|
||
|
||
# Типизированное оформление
|
||
t = atom["type"]
|
||
if t in ("stat",):
|
||
return f"📊 {text}"
|
||
if t in ("qa",):
|
||
return f"❓ {text}"
|
||
if t in ("cta",):
|
||
return f"👉 {text}"
|
||
if t in ("hook",):
|
||
return f"🔥 {text}"
|
||
if t in ("comparison",):
|
||
return f"⚖️ {text}"
|
||
if t in ("instruction",):
|
||
return f"✅ {text}"
|
||
if t in ("quote",):
|
||
return f'» {text}'
|
||
return text
|
||
|
||
|
||
def insert_versions(conn, atom_id: str, content: str) -> bool:
|
||
"""Вставить atom_versions под telegram (plain)."""
|
||
with conn.cursor() as cur:
|
||
cur.execute("""
|
||
INSERT INTO atom_versions
|
||
(atom_id, channel, format, content, quality_score)
|
||
VALUES (%s, %s, %s, %s, %s)
|
||
ON CONFLICT (atom_id, channel, format) DO NOTHING
|
||
""", (atom_id, "telegram", "plain", content, 7.0))
|
||
committed = cur.rowcount > 0
|
||
conn.commit()
|
||
return committed
|
||
|
||
|
||
def main():
|
||
ap = argparse.ArgumentParser(description="MVP stage 2: compositions (telegram)")
|
||
ap.add_argument("--channel", default="telegram", help="канал")
|
||
ap.add_argument("--format", default="plain", help="формат")
|
||
ap.add_argument("--limit", type=int, default=20, help="сколько атомов")
|
||
args = ap.parse_args()
|
||
|
||
conn = get_conn()
|
||
atoms = get_atoms(conn, args.limit)
|
||
print(f" • {len(atoms)} атомов для {args.channel}")
|
||
|
||
n = 0
|
||
start = time.time()
|
||
for atom in atoms:
|
||
if has_version(conn, str(atom["id"]), args.channel):
|
||
continue
|
||
content = build_telegram_version(atom)
|
||
if insert_versions(conn, str(atom["id"]), content):
|
||
n += 1
|
||
conn.close()
|
||
|
||
print(f" ✓ atom_versions: {n} за {time.time()-start:.1f}с")
|
||
print(f"\nMVP stage 2 compositions OK: {n} версий под {args.channel}")
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|