""" app_core.py ----------- Функция generate_video(text) → абсолютный путь к готовому MP4. ❗ Промпты PROMPT_JSON и DETAILED_PROMPT скопированы без изменений из вашего исходного ноутбука. """ # ── стандартные ─────────────────────────────────────────────── import os, json, textwrap, subprocess, tempfile from pathlib import Path from datetime import datetime # ── сторонние ───────────────────────────────────────────────── from openai import OpenAI # openai-python ≥1.33.0 from pydub import AudioSegment from playwright.sync_api import sync_playwright # ── Playwright браузер (скачиваем только один раз) ──────────── _pw_flag = Path("/tmp/.pw_chrom_installed") if not _pw_flag.exists(): subprocess.run(["playwright", "install", "chromium"], check=True) _pw_flag.touch() # ── OpenAI client (ключ берётся из секрета) ─────────────────── client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) # ─────────────────── PROMPT_JSON ──────────────────────────── PROMPT_JSON = textwrap.dedent(r""" You are a presentation-outliner. The user needs VALID json only — no extra commentary. (json!) ✦ Rules 0. Total slides ≤ 10 (including the title slide). • If the source text is longer, merge or summarise content to stay within 10 slides. 1. First slide MUST be type "title": { "slide_idx":1, "type":"title", "title":"…", "body":"" } (body may stay empty) 2. Prefer **"list"** whenever possible. • Break sentences into concise bullet-points. • Use "text" only when the content truly cannot be listed. Allowed types: "list" – array, ≤ 5 items ← _default choice_ "text" – short paragraph "quote" – short quotation or bold statement "code" – code block, copy verbatim from ``` fences 3. Preserve every ``` … ``` code block unchanged. 4. Return this exact schema: { "slides":[ { "slide_idx":N, "type":"…", "title":"…", "body":… }, … // ≤ 9 more objects after the title slide ] } Output the json only. """).strip() # ───────────────── DETAILED_PROMPT ────────────────────────── DETAILED_PROMPT = textwrap.dedent(r""" You are a friendly, motivational voice-over writer. The user needs VALID json only — no extra commentary. (json!) Source: • "raw_text" — full original article • "slides" — list of slide dictionaries (title, type, body) Task for EACH slide in order: • Write **at least two sentences** (≈ 25–60 words total). • Use the slide’s visible content **and** extra context from raw_text. • Keep a welcoming tone: encourage, explain, or add a useful tip. • Mention code or quote briefly (“In this code snippet you’ll see …”). • First slide → start with a warm greeting + slide title. • Last slide → quick recap + short friendly goodbye. Output exactly: { "narration":[ { "slide_idx":N, "voice_text":"..." }, … ] } """).strip() # ────────────────── вспомогательные функции ───────────────── def text_to_outline(raw: str) -> list: rsp = client.chat.completions.create( model="gpt-4o", temperature=0.3, response_format={"type":"json_object"}, messages=[ {"role":"system","content":PROMPT_JSON}, {"role":"user", "content":raw} ] ) return json.loads(rsp.choices[0].message.content)["slides"] def make_narration(raw: str, slides: list) -> list: rsp = client.chat.completions.create( model="gpt-4o", temperature=0.7, response_format={"type":"json_object"}, messages=[ {"role":"system","content":DETAILED_PROMPT}, {"role":"user", "content":json.dumps( {"raw_text":raw,"slides":slides}, ensure_ascii=False)} ] ) return json.loads(rsp.choices[0].message.content)["narration"] def build_html(slide: dict) -> str: import html as _h t, body = slide["type"], slide["body"] title = _h.escape(slide["title"]) if t=="title": cont=f"
“{_h.escape(str(body))}”" elif t=="code": code=_h.escape(str(body).strip().lstrip("`").rstrip("`")) cont=f"
{code}"
else:
cont=f"{_h.escape(str(body))}
" HTML_WRAP = """