Ava2lon commited on
Commit
59215bb
·
verified ·
1 Parent(s): af78cff

Upload 115 files

Browse files
.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 active Gemini model name.
57
  """
58
- model = _pick_model()
59
- logger.info(f"[Gemini] Selected model: {model}")
60
- return model
 
 
 
 
 
 
 
 
 
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 .metadata import generate_metadata
4
  from .hashtags import generate_hashtags
5
  from .thumbnail import generate_thumbnail
6
- from .scheduler import schedule_post
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 generate_metadata(source)
21
 
22
  # 2. Hashtags
23
- hashtags = await generate_hashtags(metadata)
24
 
25
  # 3. Thumbnail
26
- thumbnail = await generate_thumbnail(source)
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(webhook, json=response)
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 == "publish":
39
- if not dispatch_publish:
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
- while True:
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
- while True:
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 publisher.platforms.tiktok import TikTokPublisher
2
- from publisher.platforms.reels import ReelsPublisher
3
- from publisher.platforms.shorts import ShortsPublisher
4
- from publisher.platforms.facebook import FacebookPublisher
5
 
6
 
7
  PUBLISHERS = {
8
- "tiktok": TikTokPublisher(),
9
- "reels": ReelsPublisher(),
10
- "shorts": ShortsPublisher(),
11
- "facebook": FacebookPublisher(),
12
  }
13
 
14
 
15
- async def publish(payload):
16
-
 
 
 
 
 
 
17
  results = {}
 
 
 
 
 
 
18
 
19
- for platform in payload.platforms:
20
-
21
  publisher = PUBLISHERS.get(platform)
22
-
23
  if not publisher:
 
24
  continue
25
 
26
- results[platform] = await publisher.safe_publish(payload)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
 
28
- return results
 
 
 
 
 
 
 
 
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, timedelta
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.utcnow()
77
 
78
  for job in _tasks:
79
  if job["status"] != "queued":
80
  continue
81
 
82
- publish_time = job["payload"].get("publish_at")
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 .render import render_video
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 = render_video(job["video"], words)
63
 
64
- outputs.append(clip_path)
 
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
- output = video_path.replace(
76
- ".mp4",
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")