Upload 115 files
Browse files- .gitattributes +3 -0
- publisher/ai/gemini_client.py +14 -5
- publisher/metadata_engine.py +2 -1
- publisher/publisher.py +8 -7
- publisher/publisher_ai.py +9 -33
- publisher/router.py +62 -15
- publisher/scheduler_engine.py +35 -10
- utils/batch_queue.py +5 -5
- utils/clipper.py +19 -6
- utils/job_queue.py +19 -1
.gitattributes
CHANGED
|
@@ -35,3 +35,6 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
fonts/TikTok-Bold.ttf filter=lfs diff=lfs merge=lfs -text
|
| 37 |
gradio_demo.png filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
fonts/TikTok-Bold.ttf filter=lfs diff=lfs merge=lfs -text
|
| 37 |
gradio_demo.png filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
assets/edu_note.wav filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
assets/fun_fact.wav filter=lfs diff=lfs merge=lfs -text
|
| 40 |
+
assets/thanks.wav filter=lfs diff=lfs merge=lfs -text
|
publisher/ai/gemini_client.py
CHANGED
|
@@ -53,11 +53,20 @@ def _pick_model():
|
|
| 53 |
def get_model():
|
| 54 |
"""
|
| 55 |
Public entrypoint used by publisher_ai.
|
| 56 |
-
Returns
|
| 57 |
"""
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
|
| 62 |
|
| 63 |
def safe_generate(prompt: str, client_callable):
|
|
@@ -80,4 +89,4 @@ def safe_generate(prompt: str, client_callable):
|
|
| 80 |
logger.warning(f"[Gemini FAIL] {model}: {str(e)}")
|
| 81 |
time.sleep(0.5)
|
| 82 |
|
| 83 |
-
raise RuntimeError(f"All Gemini models failed: {last_error}")
|
|
|
|
| 53 |
def get_model():
|
| 54 |
"""
|
| 55 |
Public entrypoint used by publisher_ai.
|
| 56 |
+
Returns an initialized Gemini model.
|
| 57 |
"""
|
| 58 |
+
try:
|
| 59 |
+
import google.generativeai as genai
|
| 60 |
+
except ImportError as exc:
|
| 61 |
+
raise RuntimeError("google-generativeai is required for Gemini features") from exc
|
| 62 |
+
|
| 63 |
+
api_key = os.getenv("GEMINI_API_KEY") or os.getenv("GOOGLE_API_KEY")
|
| 64 |
+
if api_key:
|
| 65 |
+
genai.configure(api_key=api_key)
|
| 66 |
+
|
| 67 |
+
model_name = _pick_model()
|
| 68 |
+
logger.info(f"[Gemini] Selected model: {model_name}")
|
| 69 |
+
return genai.GenerativeModel(model_name)
|
| 70 |
|
| 71 |
|
| 72 |
def safe_generate(prompt: str, client_callable):
|
|
|
|
| 89 |
logger.warning(f"[Gemini FAIL] {model}: {str(e)}")
|
| 90 |
time.sleep(0.5)
|
| 91 |
|
| 92 |
+
raise RuntimeError(f"All Gemini models failed: {last_error}")
|
publisher/metadata_engine.py
CHANGED
|
@@ -7,6 +7,7 @@ def generate_metadata(video_path: str):
|
|
| 7 |
|
| 8 |
prompt = f"""
|
| 9 |
Generate viral short-form video metadata.
|
|
|
|
| 10 |
|
| 11 |
Return JSON:
|
| 12 |
title
|
|
@@ -21,4 +22,4 @@ def generate_metadata(video_path: str):
|
|
| 21 |
|
| 22 |
return {
|
| 23 |
"metadata": text
|
| 24 |
-
}
|
|
|
|
| 7 |
|
| 8 |
prompt = f"""
|
| 9 |
Generate viral short-form video metadata.
|
| 10 |
+
Source: {video_path}
|
| 11 |
|
| 12 |
Return JSON:
|
| 13 |
title
|
|
|
|
| 22 |
|
| 23 |
return {
|
| 24 |
"metadata": text
|
| 25 |
+
}
|
publisher/publisher.py
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
|
|
| 1 |
import requests
|
| 2 |
|
| 3 |
-
from .
|
| 4 |
from .hashtags import generate_hashtags
|
| 5 |
from .thumbnail import generate_thumbnail
|
| 6 |
-
from .
|
| 7 |
from .router import publish_all
|
| 8 |
|
| 9 |
|
|
@@ -17,13 +18,13 @@ async def run_publisher(payload: dict):
|
|
| 17 |
)
|
| 18 |
|
| 19 |
# 1. Metadata
|
| 20 |
-
metadata = await
|
| 21 |
|
| 22 |
# 2. Hashtags
|
| 23 |
-
hashtags = await
|
| 24 |
|
| 25 |
# 3. Thumbnail
|
| 26 |
-
thumbnail = await
|
| 27 |
|
| 28 |
# 4. Schedule
|
| 29 |
schedule_time = schedule_post(payload)
|
|
@@ -47,6 +48,6 @@ async def run_publisher(payload: dict):
|
|
| 47 |
}
|
| 48 |
|
| 49 |
if webhook:
|
| 50 |
-
requests.post
|
| 51 |
|
| 52 |
-
return response
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
import requests
|
| 3 |
|
| 4 |
+
from .metadata_engine import generate_metadata
|
| 5 |
from .hashtags import generate_hashtags
|
| 6 |
from .thumbnail import generate_thumbnail
|
| 7 |
+
from .scheduler_engine import schedule_post
|
| 8 |
from .router import publish_all
|
| 9 |
|
| 10 |
|
|
|
|
| 18 |
)
|
| 19 |
|
| 20 |
# 1. Metadata
|
| 21 |
+
metadata = await asyncio.to_thread(generate_metadata, source)
|
| 22 |
|
| 23 |
# 2. Hashtags
|
| 24 |
+
hashtags = await asyncio.to_thread(generate_hashtags, metadata)
|
| 25 |
|
| 26 |
# 3. Thumbnail
|
| 27 |
+
thumbnail = await asyncio.to_thread(generate_thumbnail, source)
|
| 28 |
|
| 29 |
# 4. Schedule
|
| 30 |
schedule_time = schedule_post(payload)
|
|
|
|
| 48 |
}
|
| 49 |
|
| 50 |
if webhook:
|
| 51 |
+
await asyncio.to_thread(requests.post, webhook, json=response, timeout=10)
|
| 52 |
|
| 53 |
+
return response
|
publisher/publisher_ai.py
CHANGED
|
@@ -35,12 +35,12 @@ async def process_job(job: Dict[str, Any]) -> Dict[str, Any]:
|
|
| 35 |
job_type = job.get("type")
|
| 36 |
payload = job.get("payload", {})
|
| 37 |
|
| 38 |
-
if job_type
|
| 39 |
-
|
| 40 |
-
raise RuntimeError("dispatch_publish not available")
|
| 41 |
|
|
|
|
| 42 |
return await dispatch_publish(
|
| 43 |
-
video_path=payload.get("video_path"),
|
| 44 |
payload=payload
|
| 45 |
)
|
| 46 |
|
|
@@ -102,11 +102,13 @@ async def autonomous_loop(seed_video_path: Optional[str] = None):
|
|
| 102 |
|
| 103 |
# 3. Process job
|
| 104 |
result = await process_job(job)
|
|
|
|
|
|
|
| 105 |
|
| 106 |
logger.info(f"[V10] Job completed: {job.get('id')}")
|
| 107 |
|
| 108 |
# 4. Optional webhook callback
|
| 109 |
-
webhook = job.get("webhook")
|
| 110 |
if webhook:
|
| 111 |
asyncio.create_task(_send_webhook(webhook, result))
|
| 112 |
|
|
@@ -170,27 +172,13 @@ async def _send_webhook(url: str, data: dict):
|
|
| 170 |
# PUBLIC ENTRYPOINT (REQUIRED BY MAIN APP)
|
| 171 |
# =====================================================
|
| 172 |
|
| 173 |
-
import asyncio
|
| 174 |
-
|
| 175 |
-
|
| 176 |
async def start_autonomous_brain():
|
| 177 |
"""
|
| 178 |
Unified startup entry for Autonomous Publisher.
|
| 179 |
Safe background loop.
|
| 180 |
"""
|
| 181 |
|
| 182 |
-
|
| 183 |
-
try:
|
| 184 |
-
# call your existing brain runner here
|
| 185 |
-
await asyncio.to_thread(run_autonomous_engine)
|
| 186 |
-
|
| 187 |
-
except Exception as e:
|
| 188 |
-
print("Autonomous brain error:", e)
|
| 189 |
-
|
| 190 |
-
# prevent CPU burn
|
| 191 |
-
await asyncio.sleep(30)
|
| 192 |
-
|
| 193 |
-
import asyncio
|
| 194 |
|
| 195 |
|
| 196 |
async def start_brain():
|
|
@@ -198,16 +186,4 @@ async def start_brain():
|
|
| 198 |
V11 Standard Publisher Entry Point
|
| 199 |
"""
|
| 200 |
|
| 201 |
-
|
| 202 |
-
try:
|
| 203 |
-
# 🔁 Replace this with your real engine function
|
| 204 |
-
# Example options:
|
| 205 |
-
# await asyncio.to_thread(run_autonomous_engine)
|
| 206 |
-
# await asyncio.to_thread(autonomous_loop)
|
| 207 |
-
# await asyncio.to_thread(run_brain)
|
| 208 |
-
|
| 209 |
-
await asyncio.sleep(10)
|
| 210 |
-
|
| 211 |
-
except Exception as e:
|
| 212 |
-
print("[Publisher Brain Error]", e)
|
| 213 |
-
await asyncio.sleep(10)
|
|
|
|
| 35 |
job_type = job.get("type")
|
| 36 |
payload = job.get("payload", {})
|
| 37 |
|
| 38 |
+
if job_type in {"publish", "auto-publish"} and not dispatch_publish:
|
| 39 |
+
raise RuntimeError("dispatch_publish not available")
|
|
|
|
| 40 |
|
| 41 |
+
if job_type == "publish":
|
| 42 |
return await dispatch_publish(
|
| 43 |
+
video_path=payload.get("video_path") or payload.get("source"),
|
| 44 |
payload=payload
|
| 45 |
)
|
| 46 |
|
|
|
|
| 102 |
|
| 103 |
# 3. Process job
|
| 104 |
result = await process_job(job)
|
| 105 |
+
job["status"] = "completed"
|
| 106 |
+
job["result"] = result
|
| 107 |
|
| 108 |
logger.info(f"[V10] Job completed: {job.get('id')}")
|
| 109 |
|
| 110 |
# 4. Optional webhook callback
|
| 111 |
+
webhook = job.get("webhook") or payload.get("webhook")
|
| 112 |
if webhook:
|
| 113 |
asyncio.create_task(_send_webhook(webhook, result))
|
| 114 |
|
|
|
|
| 172 |
# PUBLIC ENTRYPOINT (REQUIRED BY MAIN APP)
|
| 173 |
# =====================================================
|
| 174 |
|
|
|
|
|
|
|
|
|
|
| 175 |
async def start_autonomous_brain():
|
| 176 |
"""
|
| 177 |
Unified startup entry for Autonomous Publisher.
|
| 178 |
Safe background loop.
|
| 179 |
"""
|
| 180 |
|
| 181 |
+
await autonomous_loop()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
|
| 183 |
|
| 184 |
async def start_brain():
|
|
|
|
| 186 |
V11 Standard Publisher Entry Point
|
| 187 |
"""
|
| 188 |
|
| 189 |
+
await autonomous_loop()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
publisher/router.py
CHANGED
|
@@ -1,28 +1,75 @@
|
|
| 1 |
-
from
|
| 2 |
-
from
|
| 3 |
-
from
|
| 4 |
-
from
|
| 5 |
|
| 6 |
|
| 7 |
PUBLISHERS = {
|
| 8 |
-
"tiktok":
|
| 9 |
-
"reels":
|
| 10 |
-
"shorts":
|
| 11 |
-
"facebook":
|
| 12 |
}
|
| 13 |
|
| 14 |
|
| 15 |
-
async def
|
| 16 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
results = {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
-
for platform in
|
| 20 |
-
|
| 21 |
publisher = PUBLISHERS.get(platform)
|
| 22 |
-
|
| 23 |
if not publisher:
|
|
|
|
| 24 |
continue
|
| 25 |
|
| 26 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
|
| 28 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from publisher.platforms.facebook import publish_facebook
|
| 2 |
+
from publisher.platforms.reels import publish_reels
|
| 3 |
+
from publisher.platforms.shorts import publish_shorts
|
| 4 |
+
from publisher.platforms.tiktok import publish_tiktok
|
| 5 |
|
| 6 |
|
| 7 |
PUBLISHERS = {
|
| 8 |
+
"tiktok": publish_tiktok,
|
| 9 |
+
"reels": publish_reels,
|
| 10 |
+
"shorts": publish_shorts,
|
| 11 |
+
"facebook": publish_facebook,
|
| 12 |
}
|
| 13 |
|
| 14 |
|
| 15 |
+
async def publish_all(
|
| 16 |
+
source,
|
| 17 |
+
platforms,
|
| 18 |
+
metadata=None,
|
| 19 |
+
hashtags=None,
|
| 20 |
+
thumbnail=None,
|
| 21 |
+
schedule_time=None,
|
| 22 |
+
):
|
| 23 |
results = {}
|
| 24 |
+
metadata = metadata or {}
|
| 25 |
+
hashtags = hashtags or {}
|
| 26 |
+
caption = metadata.get("metadata") if isinstance(metadata, dict) else str(metadata)
|
| 27 |
+
tag_value = hashtags.get("hashtags", hashtags) if isinstance(hashtags, dict) else hashtags
|
| 28 |
+
if isinstance(tag_value, str):
|
| 29 |
+
tag_value = [tag.strip() for tag in tag_value.split(",") if tag.strip()]
|
| 30 |
|
| 31 |
+
for platform in platforms:
|
|
|
|
| 32 |
publisher = PUBLISHERS.get(platform)
|
|
|
|
| 33 |
if not publisher:
|
| 34 |
+
results[platform] = {"status": "skipped", "error": "Unsupported platform"}
|
| 35 |
continue
|
| 36 |
|
| 37 |
+
platform_payload = {
|
| 38 |
+
"video_path": source,
|
| 39 |
+
"caption": caption or "",
|
| 40 |
+
"hashtags": tag_value or [],
|
| 41 |
+
"thumbnail": thumbnail,
|
| 42 |
+
"schedule_time": schedule_time,
|
| 43 |
+
}
|
| 44 |
+
try:
|
| 45 |
+
results[platform] = await publisher(platform_payload)
|
| 46 |
+
except Exception as exc:
|
| 47 |
+
results[platform] = {"status": "failed", "error": str(exc)}
|
| 48 |
+
|
| 49 |
+
return results
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
async def publish(payload):
|
| 53 |
+
if isinstance(payload, dict):
|
| 54 |
+
source = payload.get("source") or payload.get("video_path")
|
| 55 |
+
platforms = payload.get("platforms", [])
|
| 56 |
+
metadata = payload.get("metadata")
|
| 57 |
+
hashtags = payload.get("hashtags")
|
| 58 |
+
thumbnail = payload.get("thumbnail")
|
| 59 |
+
schedule_time = payload.get("schedule_time")
|
| 60 |
+
else:
|
| 61 |
+
source = getattr(payload, "source", None) or getattr(payload, "video_path", None)
|
| 62 |
+
platforms = getattr(payload, "platforms", [])
|
| 63 |
+
metadata = getattr(payload, "metadata", None)
|
| 64 |
+
hashtags = getattr(payload, "hashtags", None)
|
| 65 |
+
thumbnail = getattr(payload, "thumbnail", None)
|
| 66 |
+
schedule_time = getattr(payload, "schedule_time", None)
|
| 67 |
|
| 68 |
+
return await publish_all(
|
| 69 |
+
source=source,
|
| 70 |
+
platforms=platforms,
|
| 71 |
+
metadata=metadata,
|
| 72 |
+
hashtags=hashtags,
|
| 73 |
+
thumbnail=thumbnail,
|
| 74 |
+
schedule_time=schedule_time,
|
| 75 |
+
)
|
publisher/scheduler_engine.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
import asyncio
|
| 2 |
import logging
|
| 3 |
-
from datetime import datetime,
|
| 4 |
|
| 5 |
logger = logging.getLogger("scheduler-engine")
|
| 6 |
|
|
@@ -48,6 +48,7 @@ def schedule_post(payload: dict):
|
|
| 48 |
|
| 49 |
job = {
|
| 50 |
"id": f"job_{len(_tasks)+1}",
|
|
|
|
| 51 |
"payload": payload,
|
| 52 |
"status": "queued",
|
| 53 |
"created_at": datetime.utcnow().isoformat()
|
|
@@ -60,6 +61,35 @@ def schedule_post(payload: dict):
|
|
| 60 |
return job
|
| 61 |
|
| 62 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
# =========================
|
| 64 |
# WORKER LOOP
|
| 65 |
# =========================
|
|
@@ -73,20 +103,15 @@ async def _worker_loop():
|
|
| 73 |
|
| 74 |
while True:
|
| 75 |
try:
|
| 76 |
-
now = datetime.
|
| 77 |
|
| 78 |
for job in _tasks:
|
| 79 |
if job["status"] != "queued":
|
| 80 |
continue
|
| 81 |
|
| 82 |
-
publish_time = job["payload"]
|
| 83 |
-
|
| 84 |
-
if not publish_time:
|
| 85 |
-
continue
|
| 86 |
-
|
| 87 |
-
publish_time = datetime.fromisoformat(publish_time)
|
| 88 |
|
| 89 |
-
if now >= publish_time:
|
| 90 |
logger.info(f"[Scheduler] Executing {job['id']}")
|
| 91 |
|
| 92 |
# mark as done (actual publish handled elsewhere)
|
|
@@ -106,4 +131,4 @@ def start_scheduler_loop():
|
|
| 106 |
"""
|
| 107 |
Optional explicit background runner.
|
| 108 |
"""
|
| 109 |
-
asyncio.create_task(_worker_loop())
|
|
|
|
| 1 |
import asyncio
|
| 2 |
import logging
|
| 3 |
+
from datetime import datetime, timezone
|
| 4 |
|
| 5 |
logger = logging.getLogger("scheduler-engine")
|
| 6 |
|
|
|
|
| 48 |
|
| 49 |
job = {
|
| 50 |
"id": f"job_{len(_tasks)+1}",
|
| 51 |
+
"type": payload.get("type", "publish"),
|
| 52 |
"payload": payload,
|
| 53 |
"status": "queued",
|
| 54 |
"created_at": datetime.utcnow().isoformat()
|
|
|
|
| 61 |
return job
|
| 62 |
|
| 63 |
|
| 64 |
+
def _publish_time(payload: dict) -> datetime | None:
|
| 65 |
+
value = payload.get("publish_at")
|
| 66 |
+
if not value:
|
| 67 |
+
return None
|
| 68 |
+
|
| 69 |
+
publish_time = datetime.fromisoformat(str(value).replace("Z", "+00:00"))
|
| 70 |
+
if publish_time.tzinfo is None:
|
| 71 |
+
publish_time = publish_time.replace(tzinfo=timezone.utc)
|
| 72 |
+
return publish_time
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
async def get_next_job():
|
| 76 |
+
"""Return the next due job and claim it for a publisher worker."""
|
| 77 |
+
now = datetime.now(timezone.utc)
|
| 78 |
+
|
| 79 |
+
for job in _tasks:
|
| 80 |
+
if job["status"] not in {"queued", "ready"}:
|
| 81 |
+
continue
|
| 82 |
+
|
| 83 |
+
publish_time = _publish_time(job["payload"])
|
| 84 |
+
if publish_time and now < publish_time:
|
| 85 |
+
continue
|
| 86 |
+
|
| 87 |
+
job["status"] = "processing"
|
| 88 |
+
return job
|
| 89 |
+
|
| 90 |
+
return None
|
| 91 |
+
|
| 92 |
+
|
| 93 |
# =========================
|
| 94 |
# WORKER LOOP
|
| 95 |
# =========================
|
|
|
|
| 103 |
|
| 104 |
while True:
|
| 105 |
try:
|
| 106 |
+
now = datetime.now(timezone.utc)
|
| 107 |
|
| 108 |
for job in _tasks:
|
| 109 |
if job["status"] != "queued":
|
| 110 |
continue
|
| 111 |
|
| 112 |
+
publish_time = _publish_time(job["payload"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
|
| 114 |
+
if publish_time is None or now >= publish_time:
|
| 115 |
logger.info(f"[Scheduler] Executing {job['id']}")
|
| 116 |
|
| 117 |
# mark as done (actual publish handled elsewhere)
|
|
|
|
| 131 |
"""
|
| 132 |
Optional explicit background runner.
|
| 133 |
"""
|
| 134 |
+
asyncio.create_task(_worker_loop())
|
utils/batch_queue.py
CHANGED
|
@@ -1,11 +1,10 @@
|
|
| 1 |
import threading
|
| 2 |
from queue import Queue
|
| 3 |
import uuid
|
| 4 |
-
import time
|
| 5 |
|
| 6 |
from .job_queue import jobs, update, notify_webhook
|
| 7 |
from .transcription import transcribe_video
|
| 8 |
-
from .
|
| 9 |
from .highlights import detect_highlights
|
| 10 |
|
| 11 |
|
|
@@ -59,9 +58,10 @@ def worker():
|
|
| 59 |
start = segment[0]["start"]
|
| 60 |
end = segment[-1]["end"]
|
| 61 |
|
| 62 |
-
clip_path =
|
| 63 |
|
| 64 |
-
|
|
|
|
| 65 |
|
| 66 |
update(job_id, progress=50 + int((i+1)/len(highlights)*40))
|
| 67 |
|
|
@@ -87,4 +87,4 @@ def worker():
|
|
| 87 |
def start_batch_worker():
|
| 88 |
|
| 89 |
t = threading.Thread(target=worker, daemon=True)
|
| 90 |
-
t.start()
|
|
|
|
| 1 |
import threading
|
| 2 |
from queue import Queue
|
| 3 |
import uuid
|
|
|
|
| 4 |
|
| 5 |
from .job_queue import jobs, update, notify_webhook
|
| 6 |
from .transcription import transcribe_video
|
| 7 |
+
from .clipper import create_clip
|
| 8 |
from .highlights import detect_highlights
|
| 9 |
|
| 10 |
|
|
|
|
| 58 |
start = segment[0]["start"]
|
| 59 |
end = segment[-1]["end"]
|
| 60 |
|
| 61 |
+
clip_path = create_clip(job["video"], start, end, i)
|
| 62 |
|
| 63 |
+
if clip_path:
|
| 64 |
+
outputs.append(clip_path)
|
| 65 |
|
| 66 |
update(job_id, progress=50 + int((i+1)/len(highlights)*40))
|
| 67 |
|
|
|
|
| 87 |
def start_batch_worker():
|
| 88 |
|
| 89 |
t = threading.Thread(target=worker, daemon=True)
|
| 90 |
+
t.start()
|
utils/clipper.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
from moviepy.editor import VideoFileClip
|
| 2 |
-
import os
|
| 3 |
import logging
|
|
|
|
| 4 |
|
| 5 |
logger = logging.getLogger(__name__)
|
| 6 |
|
|
@@ -40,6 +40,21 @@ def normalize_segments(segments):
|
|
| 40 |
})
|
| 41 |
continue
|
| 42 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
# list/tuple format
|
| 44 |
if isinstance(s, (list, tuple)) and len(s) >= 2:
|
| 45 |
try:
|
|
@@ -72,10 +87,8 @@ def create_clip(video_path, start, end, index):
|
|
| 72 |
|
| 73 |
clip = VideoFileClip(video_path).subclip(start, end)
|
| 74 |
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
f"_clip_{index}.mp4"
|
| 78 |
-
)
|
| 79 |
|
| 80 |
clip.write_videofile(
|
| 81 |
output,
|
|
@@ -127,4 +140,4 @@ def create_clips(video_path, segments):
|
|
| 127 |
logger.error(f"Segment {i} failed: {str(e)}")
|
| 128 |
continue
|
| 129 |
|
| 130 |
-
return outputs
|
|
|
|
| 1 |
from moviepy.editor import VideoFileClip
|
|
|
|
| 2 |
import logging
|
| 3 |
+
from pathlib import Path
|
| 4 |
|
| 5 |
logger = logging.getLogger(__name__)
|
| 6 |
|
|
|
|
| 40 |
})
|
| 41 |
continue
|
| 42 |
|
| 43 |
+
# grouped word segments from detect_highlights()
|
| 44 |
+
if (
|
| 45 |
+
isinstance(s, (list, tuple))
|
| 46 |
+
and s
|
| 47 |
+
and isinstance(s[0], dict)
|
| 48 |
+
and isinstance(s[-1], dict)
|
| 49 |
+
and "start" in s[0]
|
| 50 |
+
and "end" in s[-1]
|
| 51 |
+
):
|
| 52 |
+
normalized.append({
|
| 53 |
+
"start": float(s[0]["start"]),
|
| 54 |
+
"end": float(s[-1]["end"]),
|
| 55 |
+
})
|
| 56 |
+
continue
|
| 57 |
+
|
| 58 |
# list/tuple format
|
| 59 |
if isinstance(s, (list, tuple)) and len(s) >= 2:
|
| 60 |
try:
|
|
|
|
| 87 |
|
| 88 |
clip = VideoFileClip(video_path).subclip(start, end)
|
| 89 |
|
| 90 |
+
source = Path(video_path)
|
| 91 |
+
output = str(source.with_name(f"{source.stem}_clip_{index}.mp4"))
|
|
|
|
|
|
|
| 92 |
|
| 93 |
clip.write_videofile(
|
| 94 |
output,
|
|
|
|
| 140 |
logger.error(f"Segment {i} failed: {str(e)}")
|
| 141 |
continue
|
| 142 |
|
| 143 |
+
return outputs
|
utils/job_queue.py
CHANGED
|
@@ -3,6 +3,8 @@ import uuid
|
|
| 3 |
from queue import Queue
|
| 4 |
import traceback
|
| 5 |
import time
|
|
|
|
|
|
|
| 6 |
|
| 7 |
from .logger import logger
|
| 8 |
from .validators import validate_video
|
|
@@ -77,6 +79,22 @@ def update(job_id, **kwargs):
|
|
| 77 |
jobs[job_id].update(kwargs)
|
| 78 |
|
| 79 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
# =====================================================
|
| 81 |
# V7 AUTONOMOUS DIRECTOR WORKER
|
| 82 |
# =====================================================
|
|
@@ -191,4 +209,4 @@ def start_worker():
|
|
| 191 |
t = threading.Thread(target=worker, daemon=True)
|
| 192 |
t.start()
|
| 193 |
|
| 194 |
-
logger.info("[V7] Worker initialized")
|
|
|
|
| 3 |
from queue import Queue
|
| 4 |
import traceback
|
| 5 |
import time
|
| 6 |
+
import json
|
| 7 |
+
import urllib.request
|
| 8 |
|
| 9 |
from .logger import logger
|
| 10 |
from .validators import validate_video
|
|
|
|
| 79 |
jobs[job_id].update(kwargs)
|
| 80 |
|
| 81 |
|
| 82 |
+
def notify_webhook(job_id):
|
| 83 |
+
job = jobs.get(job_id)
|
| 84 |
+
if not job or not job.get("webhook"):
|
| 85 |
+
return
|
| 86 |
+
|
| 87 |
+
request = urllib.request.Request(
|
| 88 |
+
job["webhook"],
|
| 89 |
+
data=json.dumps(job).encode("utf-8"),
|
| 90 |
+
headers={"Content-Type": "application/json"},
|
| 91 |
+
)
|
| 92 |
+
try:
|
| 93 |
+
urllib.request.urlopen(request, timeout=10).close()
|
| 94 |
+
except Exception as exc:
|
| 95 |
+
logger.warning(f"Webhook delivery failed for {job_id}: {exc}")
|
| 96 |
+
|
| 97 |
+
|
| 98 |
# =====================================================
|
| 99 |
# V7 AUTONOMOUS DIRECTOR WORKER
|
| 100 |
# =====================================================
|
|
|
|
| 209 |
t = threading.Thread(target=worker, daemon=True)
|
| 210 |
t.start()
|
| 211 |
|
| 212 |
+
logger.info("[V7] Worker initialized")
|