Spaces:
Running
Running
Update app_core.py
Browse files- app_core.py +254 -117
app_core.py
CHANGED
|
@@ -1,21 +1,26 @@
|
|
| 1 |
-
# app_core.py ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 2 |
"""
|
| 3 |
-
ΠΠ΄Π½ΠΎΡΠ°ΠΉΠ»ΠΎΠ²ΡΠΉ
|
| 4 |
-
raw text β outline β HTML+PNG β OpenAI
|
| 5 |
|
| 6 |
-
ΠΠΠΠΠ: PROMPT_JSON ΠΈ DETAILED_PROMPT ΠΎΡΡΠ°Π²Π»Π΅Π½Ρ 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
"""
|
| 8 |
|
| 9 |
# β ΡΡΠ°Π½Π΄Π°ΡΡΠ½ΡΠ΅ β
|
| 10 |
-
import os, json, textwrap, subprocess, tempfile, shutil
|
| 11 |
from pathlib import Path
|
| 12 |
from datetime import datetime
|
| 13 |
|
| 14 |
-
# β third
|
| 15 |
-
import openai
|
| 16 |
from openai import OpenAI
|
| 17 |
from pydub import AudioSegment
|
| 18 |
from playwright.sync_api import sync_playwright
|
|
|
|
| 19 |
|
| 20 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 21 |
# 0. Playwright Π±ΡΠ°ΡΠ·Π΅Ρ (ΡΡΡΠ°Π½Π°Π²Π»ΠΈΠ²Π°Π΅ΠΌ 1 ΡΠ°Π· Π±Π΅Π· sudo)
|
|
@@ -26,10 +31,10 @@ if not _pw_flag.exists():
|
|
| 26 |
_pw_flag.touch()
|
| 27 |
|
| 28 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 29 |
-
# 1.
|
| 30 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 31 |
PROMPT_JSON = textwrap.dedent("""
|
| 32 |
-
You are a presentation
|
| 33 |
The user needs VALID json only β no extra commentary. (json!)
|
| 34 |
|
| 35 |
β¦ Rules
|
|
@@ -41,13 +46,13 @@ PROMPT_JSON = textwrap.dedent("""
|
|
| 41 |
(body may stay empty)
|
| 42 |
|
| 43 |
2. Prefer **"list"** whenever possible.
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
|
| 52 |
3. Preserve every ``` β¦ ``` code block unchanged.
|
| 53 |
|
|
@@ -60,10 +65,11 @@ PROMPT_JSON = textwrap.dedent("""
|
|
| 60 |
}
|
| 61 |
|
| 62 |
Output the json only.
|
|
|
|
| 63 |
""").strip()
|
| 64 |
|
| 65 |
DETAILED_PROMPT = textwrap.dedent("""
|
| 66 |
-
You are a friendly, motivational voice
|
| 67 |
The user needs VALID json only β no extra commentary. (json!)
|
| 68 |
|
| 69 |
Source:
|
|
@@ -71,7 +77,7 @@ DETAILED_PROMPT = textwrap.dedent("""
|
|
| 71 |
β’ "slides" β list of slide dictionaries (title, type, body)
|
| 72 |
|
| 73 |
Task for EACH slide in order:
|
| 74 |
-
β’ Write **at least two sentences** (β
|
| 75 |
β’ Use the slideβs visible content **and** extra context from raw_text.
|
| 76 |
β’ Keep a welcoming tone: encourage, explain, or add a useful tip.
|
| 77 |
β’ Mention code or quote briefly (βIn this code snippet youβll see β¦β).
|
|
@@ -83,115 +89,246 @@ DETAILED_PROMPT = textwrap.dedent("""
|
|
| 83 |
""").strip()
|
| 84 |
|
| 85 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 86 |
-
# 2.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 88 |
-
client = OpenAI(
|
| 89 |
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
|
|
|
|
|
|
| 93 |
temperature=0.3,
|
| 94 |
-
response_format={"type":"json_object"},
|
| 95 |
-
messages=[
|
| 96 |
-
|
|
|
|
|
|
|
|
|
|
| 97 |
)
|
| 98 |
-
|
|
|
|
|
|
|
| 99 |
|
| 100 |
def build_slide_html(slide: dict) -> str:
|
| 101 |
-
|
| 102 |
-
title
|
| 103 |
-
|
| 104 |
-
if t=="title":
|
| 105 |
-
content=f"<h1>{title}</h1>"
|
| 106 |
-
elif t=="list":
|
| 107 |
-
items="\n".join(f"<li>{
|
| 108 |
-
content=f"<h1>{title}</h1><ul>{items}</ul>"
|
| 109 |
-
elif t=="quote":
|
| 110 |
-
content=f"<blockquote>β{
|
| 111 |
-
elif t=="code":
|
| 112 |
-
code=
|
| 113 |
-
content=f"<h1>{title}</h1><pre><code>{code}</code></pre>"
|
| 114 |
-
else:
|
| 115 |
-
content=f"<h1>{title}</h1><p>{
|
| 116 |
-
|
| 117 |
-
HTML_BASE =
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
temperature=0.8,
|
| 138 |
-
response_format={"type":"json_object"},
|
| 139 |
-
messages=[
|
| 140 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
)
|
| 142 |
-
return json.loads(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
|
| 144 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 145 |
-
#
|
| 146 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 147 |
-
def generate_video(text: str) -> str:
|
| 148 |
-
"""ΠΠΎΠ·Π²ΡΠ°ΡΠ°Π΅Ρ Π°Π±ΡΠΎΠ»ΡΡΠ½ΡΠΉ ΠΏΡΡΡ ΠΊ output.mp4"""
|
| 149 |
-
work = Path(tempfile.mkdtemp(prefix="ppt2vid_"))
|
| 150 |
-
slides_dir = work/"slides"; audio_dir=work/"audio"
|
| 151 |
-
slides_dir.mkdir(); audio_dir.mkdir()
|
| 152 |
|
| 153 |
-
|
| 154 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
|
| 156 |
-
#
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
for img,d in zip(sorted(pngs),durations):
|
| 190 |
-
f.write(f"file '{img}'\n"); f.write(f"duration {d}\n")
|
| 191 |
-
f.write(f"file '{pngs[-1]}'\n")
|
| 192 |
-
|
| 193 |
-
mp4=work/"output.mp4"
|
| 194 |
-
subprocess.run(["ffmpeg","-y","-f","concat","-safe","0","-i",concat,
|
| 195 |
-
"-i",wav,"-c:v","libx264","-pix_fmt","yuv420p",
|
| 196 |
-
"-c:a","aac","-shortest",mp4],check=True)
|
| 197 |
-
return str(mp4)
|
|
|
|
|
|
|
| 1 |
"""
|
| 2 |
+
ΠΠ΄Π½ΠΎΡΠ°ΠΉΠ»ΠΎΠ²ΡΠΉ ΠΊΠΎΠ½Π²Π΅ΠΉΠ΅Ρ Π΄Π»Ρ Gradio:
|
| 3 |
+
raw text β outline β HTML+PNG β OpenAIβTTS β MP4
|
| 4 |
|
| 5 |
+
ΠΠΠΠΠ: PROMPT_JSON ΠΈ DETAILED_PROMPT ΠΎΡΡΠ°Π²Π»Π΅Π½Ρ 1βΠ²β1.
|
| 6 |
+
|
| 7 |
+
ΠΠ°ΠΏΡΡΠΊ:
|
| 8 |
+
python app_core.py # Π»ΠΎΠΊΠ°Π»ΡΠ½ΡΠΉ Π·Π°ΠΏΡΡΠΊ Gradio
|
| 9 |
+
# ΠΈΠ»ΠΈ
|
| 10 |
+
import app_core; app_core.main()
|
| 11 |
"""
|
| 12 |
|
| 13 |
# β ΡΡΠ°Π½Π΄Π°ΡΡΠ½ΡΠ΅ β
|
| 14 |
+
import os, json, textwrap, subprocess, tempfile, shutil, html, asyncio
|
| 15 |
from pathlib import Path
|
| 16 |
from datetime import datetime
|
| 17 |
|
| 18 |
+
# β thirdβparty β
|
| 19 |
+
import openai # β₯β―1.33.0
|
| 20 |
from openai import OpenAI
|
| 21 |
from pydub import AudioSegment
|
| 22 |
from playwright.sync_api import sync_playwright
|
| 23 |
+
import gradio as gr
|
| 24 |
|
| 25 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 26 |
# 0. Playwright Π±ΡΠ°ΡΠ·Π΅Ρ (ΡΡΡΠ°Π½Π°Π²Π»ΠΈΠ²Π°Π΅ΠΌ 1 ΡΠ°Π· Π±Π΅Π· sudo)
|
|
|
|
| 31 |
_pw_flag.touch()
|
| 32 |
|
| 33 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 34 |
+
# 1. System prompts (ΠΎΡΡΠ°Π²Π»Π΅Π½Ρ Π±Π΅Π· ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΠΉ)
|
| 35 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 36 |
PROMPT_JSON = textwrap.dedent("""
|
| 37 |
+
You are a presentationβoutliner.
|
| 38 |
The user needs VALID json only β no extra commentary. (json!)
|
| 39 |
|
| 40 |
β¦ Rules
|
|
|
|
| 46 |
(body may stay empty)
|
| 47 |
|
| 48 |
2. Prefer **"list"** whenever possible.
|
| 49 |
+
+ β’ Break sentences into concise bulletβpoints.
|
| 50 |
+
+ β’ Use "text" only when the content truly cannot be listed.
|
| 51 |
+
+ Allowed types:
|
| 52 |
+
+ "list" β array, β€ 5 items β _default choice_
|
| 53 |
+
+ "text" β short paragraph
|
| 54 |
+
+ "quote" β short quotation or bold statement
|
| 55 |
+
+ "code" β code block, copy verbatim from ``` fences
|
| 56 |
|
| 57 |
3. Preserve every ``` β¦ ``` code block unchanged.
|
| 58 |
|
|
|
|
| 65 |
}
|
| 66 |
|
| 67 |
Output the json only.
|
| 68 |
+
|
| 69 |
""").strip()
|
| 70 |
|
| 71 |
DETAILED_PROMPT = textwrap.dedent("""
|
| 72 |
+
You are a friendly, motivational voiceβover writer.
|
| 73 |
The user needs VALID json only β no extra commentary. (json!)
|
| 74 |
|
| 75 |
Source:
|
|
|
|
| 77 |
β’ "slides" β list of slide dictionaries (title, type, body)
|
| 78 |
|
| 79 |
Task for EACH slide in order:
|
| 80 |
+
β’ Write **at least two sentences** (ββ―25β60 words total).
|
| 81 |
β’ Use the slideβs visible content **and** extra context from raw_text.
|
| 82 |
β’ Keep a welcoming tone: encourage, explain, or add a useful tip.
|
| 83 |
β’ Mention code or quote briefly (βIn this code snippet youβll see β¦β).
|
|
|
|
| 89 |
""").strip()
|
| 90 |
|
| 91 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 92 |
+
# 2. HTML/CSS ΡΠ°Π±Π»ΠΎΠ½ ΡΠ»Π°ΠΉΠ΄ΠΎΠ² (Π±Π΅Π· ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΠΉ)
|
| 93 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 94 |
+
HTML_BASE = """
|
| 95 |
+
<!DOCTYPE html>
|
| 96 |
+
<html>
|
| 97 |
+
<head>
|
| 98 |
+
<meta charset=\"utf-8\">
|
| 99 |
+
<title>{title}</title>
|
| 100 |
+
|
| 101 |
+
<!-- Hyperskill brandβlike styling -->
|
| 102 |
+
<link href=\"https://fonts.googleapis.com/css2?family=PT+Root+UI:wght@400;700&display=swap\" rel=\"stylesheet\">
|
| 103 |
+
|
| 104 |
+
<style>
|
| 105 |
+
:root {{
|
| 106 |
+
--grad-from:#4BFFDF;
|
| 107 |
+
--grad-to:#7AB7FE;
|
| 108 |
+
--accent:#6C63FF;
|
| 109 |
+
--text-dark:#000;
|
| 110 |
+
--bg-light:#fff;
|
| 111 |
+
}}
|
| 112 |
+
|
| 113 |
+
body {{
|
| 114 |
+
margin:0;
|
| 115 |
+
width:1280px;height:720px;
|
| 116 |
+
display:flex;flex-direction:column;
|
| 117 |
+
justify-content:center;align-items:center;
|
| 118 |
+
font-family:'PT Root UI',Arial,sans-serif;
|
| 119 |
+
color:var(--text-dark);
|
| 120 |
+
background:var(--bg-light);
|
| 121 |
+
}}
|
| 122 |
+
|
| 123 |
+
.wrap {{max-width:1000px;text-align:center;padding:0 40px;}}
|
| 124 |
+
|
| 125 |
+
h1 {{
|
| 126 |
+
font-size:60px;font-weight:700;margin:0 0 40px;
|
| 127 |
+
color:var(--accent);
|
| 128 |
+
}}
|
| 129 |
+
|
| 130 |
+
p {{font-size:36px;margin:0;}}
|
| 131 |
+
|
| 132 |
+
ul {{
|
| 133 |
+
font-size:34px;text-align:left;margin:0 auto;padding-left:40px;
|
| 134 |
+
}}
|
| 135 |
+
li {{margin:12px 0;}}
|
| 136 |
+
|
| 137 |
+
blockquote {{
|
| 138 |
+
font-size:40px;font-style:italic;margin:0;
|
| 139 |
+
border-left:6px solid var(--accent);padding-left:24px;
|
| 140 |
+
}}
|
| 141 |
+
|
| 142 |
+
pre {{
|
| 143 |
+
font-size:28px;line-height:1.35;margin:0;padding:24px;
|
| 144 |
+
background:#f5f5f5;border-radius:10px;text-align:left;overflow-x:auto;
|
| 145 |
+
}}
|
| 146 |
+
</style>
|
| 147 |
+
</head>
|
| 148 |
+
<body>
|
| 149 |
+
<div class=\"wrap\">
|
| 150 |
+
{content}
|
| 151 |
+
</div>
|
| 152 |
+
</body>
|
| 153 |
+
</html>
|
| 154 |
+
"""
|
| 155 |
+
|
| 156 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 157 |
+
# 3. Π€ΡΠ½ΠΊΡΠΈΠΈ ΠΊΠΎΠ½Π²Π΅ΠΉΠ΅ΡΠ°
|
| 158 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 159 |
+
client = OpenAI() # ΠΊΠ»ΡΡ Π±Π΅ΡΡΡΡΡ ΠΈΠ· OPENAI_API_KEY
|
| 160 |
|
| 161 |
+
|
| 162 |
+
def text_to_outline(raw_text: str, model: str = "gpt-4o") -> list:
|
| 163 |
+
"""GPT β ΡΠΏΠΈΡΠΎΠΊ ΡΠ»ΠΎΠ²Π°ΡΠ΅ΠΉ ΡΠ»Π°ΠΉΠ΄ΠΎΠ²"""
|
| 164 |
+
resp = client.chat.completions.create(
|
| 165 |
+
model=model,
|
| 166 |
temperature=0.3,
|
| 167 |
+
response_format={"type": "json_object"},
|
| 168 |
+
messages=[
|
| 169 |
+
{"role": "system", "content": PROMPT_JSON},
|
| 170 |
+
{"role": "user", "content": raw_text}
|
| 171 |
+
],
|
| 172 |
+
max_tokens=2048,
|
| 173 |
)
|
| 174 |
+
slides = json.loads(resp.choices[0].message.content)["slides"]
|
| 175 |
+
return slides
|
| 176 |
+
|
| 177 |
|
| 178 |
def build_slide_html(slide: dict) -> str:
|
| 179 |
+
t, body = slide["type"], slide["body"]
|
| 180 |
+
title = html.escape(slide["title"])
|
| 181 |
+
|
| 182 |
+
if t == "title":
|
| 183 |
+
content = f"<h1>{title}</h1>"
|
| 184 |
+
elif t == "list":
|
| 185 |
+
items = "\n".join(f"<li>{html.escape(str(it))}</li>" for it in body)
|
| 186 |
+
content = f"<h1>{title}</h1><ul>{items}</ul>"
|
| 187 |
+
elif t == "quote":
|
| 188 |
+
content = f"<blockquote>β{html.escape(str(body))}β</blockquote>"
|
| 189 |
+
elif t == "code":
|
| 190 |
+
code = html.escape(str(body).strip().lstrip("`").rstrip("`"))
|
| 191 |
+
content = f"<h1>{title}</h1><pre><code>{code}</code></pre>"
|
| 192 |
+
else: # text
|
| 193 |
+
content = f"<h1>{title}</h1><p>{html.escape(str(body))}</p>"
|
| 194 |
+
|
| 195 |
+
return HTML_BASE.format(title=title, content=content)
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
def save_html(slides: list, slides_dir: Path) -> list:
|
| 199 |
+
html_paths = []
|
| 200 |
+
for s in slides:
|
| 201 |
+
f = slides_dir / f"slide_{s['slide_idx']:03}.html"
|
| 202 |
+
f.write_text(build_slide_html(s), encoding="utf-8")
|
| 203 |
+
html_paths.append(f)
|
| 204 |
+
return html_paths
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
def html_to_png(html_paths: list):
|
| 208 |
+
png_paths = []
|
| 209 |
+
with sync_playwright() as p:
|
| 210 |
+
browser = p.chromium.launch()
|
| 211 |
+
page = browser.new_page(viewport={"width":1280, "height":720})
|
| 212 |
+
for f in html_paths:
|
| 213 |
+
page.goto(f.as_uri())
|
| 214 |
+
png_path = f.with_suffix(".png")
|
| 215 |
+
page.screenshot(path=png_path)
|
| 216 |
+
png_paths.append(png_path)
|
| 217 |
+
browser.close()
|
| 218 |
+
return png_paths
|
| 219 |
+
|
| 220 |
+
|
| 221 |
+
def generate_narration(raw_text: str, slides: list, model: str = "gpt-4o") -> list:
|
| 222 |
+
resp = client.chat.completions.create(
|
| 223 |
+
model=model,
|
| 224 |
temperature=0.8,
|
| 225 |
+
response_format={"type": "json_object"},
|
| 226 |
+
messages=[
|
| 227 |
+
{"role": "system", "content": DETAILED_PROMPT},
|
| 228 |
+
{"role": "user", "content": json.dumps({
|
| 229 |
+
"raw_text": raw_text,
|
| 230 |
+
"slides": slides
|
| 231 |
+
}, ensure_ascii=False)}
|
| 232 |
+
],
|
| 233 |
+
max_tokens=2048,
|
| 234 |
)
|
| 235 |
+
return json.loads(resp.choices[0].message.content)["narration"]
|
| 236 |
+
|
| 237 |
+
|
| 238 |
+
def tts_narration(narration_list: list, audio_dir: Path):
|
| 239 |
+
audio_dir.mkdir(exist_ok=True)
|
| 240 |
+
wav_paths, durations = [], []
|
| 241 |
+
|
| 242 |
+
for item in narration_list:
|
| 243 |
+
idx, text = item["slide_idx"], item["voice_text"]
|
| 244 |
+
speech = client.audio.speech.create(
|
| 245 |
+
model="tts-1",
|
| 246 |
+
voice="alloy",
|
| 247 |
+
input=text,
|
| 248 |
+
response_format="wav"
|
| 249 |
+
)
|
| 250 |
+
wav_path = audio_dir / f"slide_{idx:03}.wav"
|
| 251 |
+
speech.stream_to_file(wav_path)
|
| 252 |
+
wav_paths.append(wav_path)
|
| 253 |
+
|
| 254 |
+
snd = AudioSegment.from_file(wav_path)
|
| 255 |
+
durations.append(round(snd.duration_seconds, 2))
|
| 256 |
+
|
| 257 |
+
# glue together
|
| 258 |
+
combined = AudioSegment.empty()
|
| 259 |
+
for w in sorted(wav_paths):
|
| 260 |
+
combined += AudioSegment.from_file(w)
|
| 261 |
+
|
| 262 |
+
final_wav = audio_dir / "narration.wav"
|
| 263 |
+
combined.export(final_wav, format="wav")
|
| 264 |
+
|
| 265 |
+
return final_wav, durations
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
def assemble_video(slides_dir: Path, audio_path: Path, durations: list, output_mp4: Path):
|
| 269 |
+
png_files = sorted(slides_dir.glob("slide_*.png"))
|
| 270 |
+
if not png_files:
|
| 271 |
+
raise RuntimeError("PNG slides not found")
|
| 272 |
+
|
| 273 |
+
concat_file = slides_dir / "slides.txt"
|
| 274 |
+
with concat_file.open("w") as f:
|
| 275 |
+
for img, dur in zip(png_files, durations):
|
| 276 |
+
f.write(f"file '{img}'\n")
|
| 277 |
+
f.write(f"duration {dur}\n")
|
| 278 |
+
f.write(f"file '{png_files[-1]}'\n") # repeat last frame
|
| 279 |
+
|
| 280 |
+
ffmpeg_cmd = [
|
| 281 |
+
"ffmpeg", "-y",
|
| 282 |
+
"-f", "concat", "-safe", "0", "-i", str(concat_file),
|
| 283 |
+
"-i", str(audio_path),
|
| 284 |
+
"-c:v", "libx264", "-pix_fmt", "yuv420p",
|
| 285 |
+
"-c:a", "aac", "-shortest",
|
| 286 |
+
str(output_mp4)
|
| 287 |
+
]
|
| 288 |
+
subprocess.run(ffmpeg_cmd, check=True)
|
| 289 |
+
return output_mp4
|
| 290 |
|
| 291 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 292 |
+
# 4. ΠΠ»Π°Π²Π½Π°Ρ ΡΡΠ½ΠΊΡΠΈΡ ΠΊΠΎΠ½Π²Π΅ΠΉΠ΅ΡΠ° (Π²ΡΠ·ΡΠ²Π°Π΅ΡΡΡ ΠΈΠ· Gradio)
|
| 293 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 294 |
|
| 295 |
+
def process_pipeline(raw_text: str) -> str:
|
| 296 |
+
"""ΠΠΎΠ»Π½ΡΠΉ Π·Π°ΠΏΡΡΠΊ: Π²ΠΎΠ·Π²ΡΠ°ΡΠ°Π΅Ρ ΠΏΡΡΡ ΠΊ MP4"""
|
| 297 |
+
run_dir = Path(tempfile.mkdtemp(prefix="ai_presentation_"))
|
| 298 |
+
slides_dir = run_dir / "slides"
|
| 299 |
+
audio_dir = run_dir / "audio"
|
| 300 |
+
slides_dir.mkdir()
|
| 301 |
|
| 302 |
+
# 1. GPT β outline
|
| 303 |
+
slides = text_to_outline(raw_text)
|
| 304 |
+
|
| 305 |
+
# 2. outline β HTML β PNG
|
| 306 |
+
html_paths = save_html(slides, slides_dir)
|
| 307 |
+
html_to_png(html_paths)
|
| 308 |
+
|
| 309 |
+
# 3. GPT β narration β TTS
|
| 310 |
+
narration = generate_narration(raw_text, slides)
|
| 311 |
+
wav_path, durations = tts_narration(narration, audio_dir)
|
| 312 |
+
|
| 313 |
+
# 4. PNG + WAV β MP4
|
| 314 |
+
output_mp4 = run_dir / "output.mp4"
|
| 315 |
+
assemble_video(slides_dir, wav_path, durations, output_mp4)
|
| 316 |
+
|
| 317 |
+
return str(output_mp4)
|
| 318 |
+
|
| 319 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 320 |
+
# 5. Gradio UI
|
| 321 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 322 |
+
|
| 323 |
+
def main():
|
| 324 |
+
iface = gr.Interface(
|
| 325 |
+
fn=process_pipeline,
|
| 326 |
+
inputs=gr.Textbox(lines=20, label="Raw article / script text"),
|
| 327 |
+
outputs=gr.File(label="Generated presentation (MP4)"),
|
| 328 |
+
title="AI Presentation Generator",
|
| 329 |
+
description="Oneβclick conversion of raw text into narrated video slides."
|
| 330 |
+
)
|
| 331 |
+
iface.launch()
|
| 332 |
+
|
| 333 |
+
if __name__ == "__main__":
|
| 334 |
+
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|