karthikeya1212 commited on
Commit
eb8c5e1
Β·
verified Β·
1 Parent(s): 39a60c5

Upload 24 files

Browse files
.env ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ OPENROUTER_API_KEY=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
2
+
Dockerfile ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Base image with Python
2
+ FROM python:3.10-slim
3
+
4
+ # Set working directory
5
+ WORKDIR /app
6
+
7
+ # Environment variables
8
+ ENV PYTHONUNBUFFERED=1
9
+ ENV MODEL_DIR=/tmp/models/realvisxl_v4
10
+
11
+ # Install system dependencies
12
+ RUN apt-get update && apt-get install -y \
13
+ git \
14
+ wget \
15
+ curl \
16
+ && rm -rf /var/lib/apt/lists/*
17
+
18
+ # Copy all project files
19
+ COPY . /app
20
+
21
+ # Create model directory
22
+ RUN mkdir -p ${MODEL_DIR}
23
+
24
+ # --- Install Python dependencies ---
25
+ RUN pip install --no-cache-dir \
26
+ annotated-types==0.7.0 \
27
+ anyio==4.11.0 \
28
+ certifi==2025.10.5 \
29
+ click==8.3.0 \
30
+ colorama==0.4.6 \
31
+ fastapi==0.119.0 \
32
+ h11==0.16.0 \
33
+ httpcore==1.0.9 \
34
+ httpx==0.28.1 \
35
+ idna==3.11 \
36
+ pydantic==2.12.2 \
37
+ pydantic_core==2.41.4 \
38
+ python-dotenv==1.1.1 \
39
+ sniffio==1.3.1 \
40
+ starlette==0.48.0 \
41
+ typing_extensions==4.15.0 \
42
+ typing-inspection==0.4.2 \
43
+ uvicorn==0.37.0 \
44
+ torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cpu \
45
+ diffusers==0.30.3 \
46
+ huggingface_hub==0.26.2 \
47
+ accelerate==1.1.1 \
48
+ safetensors==0.4.5 \
49
+ pillow==10.4.0
50
+
51
+ # --- Pre-download model into /tmp/models/realvisxl_v4 ---
52
+ RUN python -c "\
53
+ import torch; \
54
+ from diffusers import StableDiffusionXLPipeline; \
55
+ from pathlib import Path; \
56
+ model_dir = Path('${MODEL_DIR}'); \
57
+ if not model_dir.exists(): \
58
+ print('Downloading model...'); \
59
+ pipe = StableDiffusionXLPipeline.from_pretrained('SG161222/RealVisXL_V4.0', torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32); \
60
+ pipe.save_pretrained(model_dir); \
61
+ print('Model downloaded to', model_dir); \
62
+ else: \
63
+ print('Model already exists at', model_dir); \
64
+ "
65
+
66
+ # Expose the app port
67
+ EXPOSE 8000
68
+
69
+ # Command to run FastAPI server
70
+ CMD ["uvicorn", "api.server:app", "--host", "0.0.0.0", "--port", "8000"]
api/__init__.py ADDED
File without changes
api/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (159 Bytes). View file
 
api/__pycache__/server.cpython-311.pyc ADDED
Binary file (5.65 kB). View file
 
api/server.py ADDED
@@ -0,0 +1,403 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # # api/server.py
2
+ # from fastapi import FastAPI, HTTPException
3
+ # from pydantic import BaseModel
4
+ # import asyncio
5
+ # from pipeline.pipeline_runner import run_pipeline_task
6
+
7
+ # app = FastAPI(title="AI ADD Maker API", version="1.0")
8
+
9
+ # # Request body schema
10
+ # class GenerateRequest(BaseModel):
11
+ # idea: str
12
+
13
+ # # Response schema (optional, for docs clarity)
14
+ # class GenerateResponse(BaseModel):
15
+ # task_id: str
16
+ # scenes: list
17
+ # images: list
18
+ # video: dict
19
+ # music: dict
20
+ # final_output: dict
21
+
22
+ # @app.post("/generate", response_model=GenerateResponse)
23
+ # async def generate_video(request: GenerateRequest):
24
+ # """
25
+ # Trigger the AI Ad/Video pipeline with a product or idea description.
26
+ # Returns all intermediate and final outputs.
27
+ # """
28
+ # idea = request.idea.strip()
29
+ # if not idea:
30
+ # raise HTTPException(status_code=400, detail="Idea cannot be empty.")
31
+
32
+ # try:
33
+ # # Run the pipeline asynchronously
34
+ # result = await run_pipeline_task(idea)
35
+ # return result
36
+ # except Exception as e:
37
+ # raise HTTPException(status_code=500, detail=f"Pipeline failed: {str(e)}")
38
+
39
+ # # Optional health check endpoint
40
+ # @app.get("/health")
41
+ # async def health_check():
42
+ # return {"status": "ok"}
43
+
44
+
45
+
46
+
47
+
48
+ # # server.py
49
+ # import uuid
50
+ # import asyncio
51
+ # from fastapi import FastAPI, HTTPException
52
+ # from pydantic import BaseModel
53
+ # from queue import QueueManager # your queue.py module
54
+ # import logging
55
+
56
+ # # -------------------------------
57
+ # # Setup logging
58
+ # # -------------------------------
59
+ # logging.basicConfig(
60
+ # level=logging.INFO,
61
+ # format="%(asctime)s [%(levelname)s] %(message)s"
62
+ # )
63
+
64
+ # # -------------------------------
65
+ # # FastAPI app
66
+ # # -------------------------------
67
+ # app = FastAPI(title="ADD Maker Server", version="1.0")
68
+
69
+ # # -------------------------------
70
+ # # Pydantic models
71
+ # # -------------------------------
72
+ # class IdeaRequest(BaseModel):
73
+ # idea: str
74
+
75
+ # class ConfirmationRequest(BaseModel):
76
+ # task_id: str
77
+ # confirm: bool
78
+
79
+ # # -------------------------------
80
+ # # Queue Manager Instance
81
+ # # -------------------------------
82
+ # queue_manager = QueueManager()
83
+
84
+ # # In-memory task storage for confirmation
85
+ # pending_confirmations = {} # task_id -> asyncio.Event
86
+
87
+ # # -------------------------------
88
+ # # Helper function for confirmation
89
+ # # -------------------------------
90
+ # async def wait_for_confirmation(task_id: str, timeout: int = 120):
91
+ # """Wait for user confirmation or auto-confirm after timeout."""
92
+ # event = asyncio.Event()
93
+ # pending_confirmations[task_id] = event
94
+ # try:
95
+ # await asyncio.wait_for(event.wait(), timeout=timeout)
96
+ # logging.info(f"Task {task_id} confirmed by user.")
97
+ # return True
98
+ # except asyncio.TimeoutError:
99
+ # logging.info(f"Task {task_id} auto-confirmed after {timeout} seconds.")
100
+ # return True
101
+ # finally:
102
+ # pending_confirmations.pop(task_id, None)
103
+
104
+ # # -------------------------------
105
+ # # API Endpoints
106
+ # # -------------------------------
107
+ # @app.post("/submit_idea")
108
+ # async def submit_idea(request: IdeaRequest):
109
+ # task_id = str(uuid.uuid4())
110
+ # logging.info(f"Received idea: {request.idea} | Task ID: {task_id}")
111
+
112
+ # # Push task to queue
113
+ # await queue_manager.enqueue({
114
+ # "task_id": task_id,
115
+ # "idea": request.idea
116
+ # })
117
+
118
+ # # Start confirmation wait in background
119
+ # asyncio.create_task(wait_for_confirmation(task_id))
120
+
121
+ # return {"status": "submitted", "task_id": task_id, "message": "Idea received, waiting for confirmation."}
122
+
123
+ # @app.post("/confirm")
124
+ # async def confirm_task(request: ConfirmationRequest):
125
+ # task_id = request.task_id
126
+ # if task_id not in pending_confirmations:
127
+ # raise HTTPException(status_code=404, detail="Task not pending confirmation or already confirmed.")
128
+
129
+ # if request.confirm:
130
+ # pending_confirmations[task_id].set()
131
+ # return {"status": "confirmed", "task_id": task_id}
132
+ # else:
133
+ # return {"status": "rejected", "task_id": task_id}
134
+
135
+ # @app.get("/")
136
+ # async def health_check():
137
+ # return {"status": "running"}
138
+
139
+ # # -------------------------------
140
+ # # Startup / Shutdown events
141
+ # # -------------------------------
142
+ # @app.on_event("startup")
143
+ # async def startup_event():
144
+ # logging.info("Server starting up...")
145
+
146
+ # @app.on_event("shutdown")
147
+ # async def shutdown_event():
148
+ # logging.info("Server shutting down...")
149
+
150
+
151
+
152
+ # # best
153
+ # # server.py
154
+ # import uuid
155
+ # import asyncio
156
+ # from fastapi import FastAPI, HTTPException
157
+ # from pydantic import BaseModel
158
+ # from services import queue_manager as queue_manager # βœ… import your actual queue module
159
+ # import logging
160
+ # from fastapi.middleware.cors import CORSMiddleware
161
+
162
+
163
+ # # -------------------------------
164
+ # # Setup logging
165
+ # # -------------------------------
166
+ # logging.basicConfig(
167
+ # level=logging.INFO,
168
+ # format="%(asctime)s [%(levelname)s] %(message)s"
169
+ # )
170
+
171
+ # # -------------------------------
172
+ # # FastAPI app
173
+ # # -------------------------------
174
+ # app = FastAPI(title="AI ADD Generator Server", version="1.0")
175
+ # # Enable CORS for local testing
176
+ # app.add_middleware(
177
+ # CORSMiddleware,
178
+ # allow_origins=["*"], # Allow requests from anywhere (for testing)
179
+ # allow_credentials=True,
180
+ # allow_methods=["*"], # Allow all HTTP methods
181
+ # allow_headers=["*"], # Allow all headers
182
+ # )
183
+
184
+ # # -------------------------------
185
+ # # Pydantic models
186
+ # # -------------------------------
187
+ # class IdeaRequest(BaseModel):
188
+ # idea: str
189
+
190
+ # class ConfirmationRequest(BaseModel):
191
+ # task_id: str
192
+ # confirm: bool
193
+
194
+ # # -------------------------------
195
+ # # In-memory confirmation tracker
196
+ # # -------------------------------
197
+ # pending_confirmations = {} # task_id -> asyncio.Event
198
+
199
+ # # -------------------------------
200
+ # # Helper function for confirmation
201
+ # # -------------------------------
202
+ # async def wait_for_confirmation(task_id: str, timeout: int = 120):
203
+ # """Wait for user confirmation or auto-confirm after timeout."""
204
+ # event = asyncio.Event()
205
+ # pending_confirmations[task_id] = event
206
+ # try:
207
+ # await asyncio.wait_for(event.wait(), timeout=timeout)
208
+ # logging.info(f"βœ… Task {task_id} confirmed by user.")
209
+ # await queue_manager.confirm_task(task_id)
210
+ # return True
211
+ # except asyncio.TimeoutError:
212
+ # logging.info(f"βŒ› Task {task_id} auto-confirmed after {timeout}s.")
213
+ # await queue_manager.confirm_task(task_id)
214
+ # return True
215
+ # finally:
216
+ # pending_confirmations.pop(task_id, None)
217
+
218
+ # # -------------------------------
219
+ # # API Endpoints
220
+ # # -------------------------------
221
+
222
+ # @app.post("/submit_idea")
223
+ # async def submit_idea(request: IdeaRequest):
224
+ # """Receives a new ad idea and enqueues it."""
225
+ # task_id = await queue_manager.add_task(request.idea)
226
+ # logging.info(f"πŸ’‘ New idea received | Task ID: {task_id}")
227
+
228
+ # # Start confirmation listener
229
+ # asyncio.create_task(wait_for_confirmation(task_id))
230
+
231
+ # return {
232
+ # "status": "submitted",
233
+ # "task_id": task_id,
234
+ # "message": "Idea received. Waiting for user confirmation after script generation."
235
+ # }
236
+
237
+
238
+ # @app.post("/confirm")
239
+ # async def confirm_task(request: ConfirmationRequest):
240
+ # """Confirms a paused task and continues the pipeline."""
241
+ # task_id = request.task_id
242
+ # if task_id not in pending_confirmations:
243
+ # raise HTTPException(status_code=404, detail="Task not pending confirmation or already confirmed.")
244
+
245
+ # if request.confirm:
246
+ # pending_confirmations[task_id].set()
247
+ # return {"status": "confirmed", "task_id": task_id}
248
+ # else:
249
+ # return {"status": "rejected", "task_id": task_id}
250
+
251
+
252
+ # @app.get("/status/{task_id}")
253
+ # async def get_status(task_id: str):
254
+ # """Check the current status of a task."""
255
+ # status = queue_manager.get_task_status(task_id)
256
+ # if not status:
257
+ # raise HTTPException(status_code=404, detail="Task not found.")
258
+ # return status
259
+
260
+
261
+ # @app.get("/")
262
+ # async def health_check():
263
+ # return {"status": "running", "message": "AI ADD Generator is live."}
264
+
265
+
266
+ # # -------------------------------
267
+ # # Startup / Shutdown events
268
+ # # -------------------------------
269
+ # @app.on_event("startup")
270
+ # async def startup_event():
271
+ # logging.info("πŸš€ Server starting up...")
272
+ # queue_manager.start_worker() # βœ… Start async worker loop
273
+
274
+ # @app.on_event("shutdown")
275
+ # async def shutdown_event():
276
+ # logging.info("πŸ›‘ Server shutting down...")
277
+
278
+
279
+
280
+ import uuid
281
+ import asyncio
282
+ from fastapi import FastAPI, HTTPException
283
+ from pydantic import BaseModel
284
+ from services import queue_manager # βœ… import your actual queue module
285
+ import logging
286
+ from fastapi.middleware.cors import CORSMiddleware
287
+
288
+ # -------------------------------
289
+ # Setup logging
290
+ # -------------------------------
291
+ logging.basicConfig(
292
+ level=logging.INFO,
293
+ format="%(asctime)s [%(levelname)s] %(message)s"
294
+ )
295
+
296
+ # -------------------------------
297
+ # FastAPI app
298
+ # -------------------------------
299
+ app = FastAPI(title="AI ADD Generator Server", version="1.0")
300
+
301
+ # Enable CORS for local testing
302
+ app.add_middleware(
303
+ CORSMiddleware,
304
+ allow_origins=["*"],
305
+ allow_credentials=True,
306
+ allow_methods=["*"],
307
+ allow_headers=["*"],
308
+ )
309
+
310
+ # -------------------------------
311
+ # Pydantic models
312
+ # -------------------------------
313
+ class IdeaRequest(BaseModel):
314
+ idea: str
315
+
316
+ class ConfirmationRequest(BaseModel):
317
+ task_id: str
318
+ confirm: bool
319
+
320
+ # -------------------------------
321
+ # In-memory confirmation tracker
322
+ # -------------------------------
323
+ pending_confirmations = {} # task_id -> asyncio.Event
324
+ script_results = {} # task_id -> generated script for confirmation
325
+
326
+ # -------------------------------
327
+ # API Endpoints
328
+ # -------------------------------
329
+ @app.post("/submit_idea")
330
+ async def submit_idea(request: IdeaRequest):
331
+ """Receives a new ad idea and enqueues it."""
332
+ task_id = await queue_manager.add_task(request.idea)
333
+ logging.info(f"πŸ’‘ New idea received | Task ID: {task_id}")
334
+
335
+ # Start worker listener
336
+ asyncio.create_task(queue_manager.wait_for_script(task_id, script_results))
337
+
338
+ return {
339
+ "status": "submitted",
340
+ "task_id": task_id,
341
+ "message": "Idea received. Script will be generated shortly.",
342
+ }
343
+
344
+ @app.post("/confirm")
345
+ async def confirm_task(request: ConfirmationRequest):
346
+ """Confirms a paused task, generates story, and returns full JSON."""
347
+ task_id = request.task_id
348
+ task = queue_manager.get_task_status(task_id)
349
+ if not task:
350
+ raise HTTPException(status_code=404, detail="Task not found.")
351
+
352
+ if task["status"] != queue_manager.TaskStatus.WAITING_CONFIRMATION:
353
+ raise HTTPException(status_code=400, detail="Task not waiting for confirmation.")
354
+
355
+ if request.confirm:
356
+ # Confirm task
357
+ await queue_manager.confirm_task(task_id)
358
+ logging.info(f"βœ… Task {task_id} confirmed by user.")
359
+
360
+ # Generate story immediately
361
+ script_result = task["result"]["script"]
362
+ story_result = await queue_manager.generate_story_after_confirm(script_result)
363
+ task["result"]["story_script"] = story_result
364
+ task["status"] = queue_manager.TaskStatus.COMPLETED
365
+
366
+ logging.info(f"🎬 Task {task_id} story generated and task completed.")
367
+ return {"status": "completed", "task": task}
368
+
369
+ else:
370
+ task["status"] = queue_manager.TaskStatus.FAILED
371
+ return {"status": "rejected", "task_id": task_id}
372
+
373
+
374
+ @app.get("/status/{task_id}")
375
+ async def get_status(task_id: str):
376
+ """Check the current status of a task."""
377
+ task = queue_manager.get_task_status(task_id)
378
+ if not task:
379
+ raise HTTPException(status_code=404, detail="Task not found.")
380
+
381
+ # If waiting confirmation, return script only
382
+ if task["status"] == queue_manager.TaskStatus.WAITING_CONFIRMATION:
383
+ return {"status": task["status"], "script": task["result"]["script"]}
384
+
385
+ return task
386
+
387
+
388
+ @app.get("/")
389
+ async def health_check():
390
+ return {"status": "running", "message": "AI ADD Generator is live."}
391
+
392
+
393
+ # -------------------------------
394
+ # Startup / Shutdown events
395
+ # -------------------------------
396
+ @app.on_event("startup")
397
+ async def startup_event():
398
+ logging.info("πŸš€ Server starting up...")
399
+ queue_manager.start_worker()
400
+
401
+ @app.on_event("shutdown")
402
+ async def shutdown_event():
403
+ logging.info("πŸ›‘ Server shutting down...")
core/__init__.py ADDED
File without changes
core/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (160 Bytes). View file
 
core/__pycache__/script_gen.cpython-311.pyc ADDED
Binary file (3 kB). View file
 
core/__pycache__/story_script.cpython-311.pyc ADDED
Binary file (7.89 kB). View file
 
core/assembler.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # core/assembler.py
2
+ """
3
+ Assembler combines video + music and optionally overlays images/text,
4
+ and produces final assembled output. Uses ffmpeg for merging audio/video.
5
+ Returns a dict with final path and metadata.
6
+ """
7
+ import subprocess
8
+ import time
9
+ from pathlib import Path
10
+ from typing import Dict
11
+ from config import OUTPUT_DIR
12
+
13
+ OUTPUT_DIR = Path(OUTPUT_DIR)
14
+ OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
15
+
16
+ def assemble_video(video_result: Dict, music_result: Dict, out_name: str = None) -> Dict:
17
+ """
18
+ video_result: dict returned from video_generator
19
+ music_result: dict returned from music_generator
20
+ Returns dict { "final_path":..., "duration":..., "meta":... }
21
+ """
22
+ t0 = time.time()
23
+ video_path = Path(video_result.get("video_path"))
24
+ music_path = Path(music_result.get("music_path"))
25
+ out_name = out_name or f"final_{int(time.time()*1000)}.mp4"
26
+ out_path = OUTPUT_DIR / out_name
27
+
28
+ # If ffmpeg is available, merge audio and video
29
+ if video_path.exists() and music_path.exists():
30
+ cmd = [
31
+ "ffmpeg", "-y",
32
+ "-i", str(video_path),
33
+ "-i", str(music_path),
34
+ "-c:v", "copy", # copy video stream
35
+ "-c:a", "aac",
36
+ "-shortest",
37
+ str(out_path)
38
+ ]
39
+ try:
40
+ subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
41
+ duration = video_result.get("duration", 0.0)
42
+ except Exception as e:
43
+ print(f"[assembler] ffmpeg merge failed: {e}. Creating placeholder final output.")
44
+ with open(out_path, "wb") as f:
45
+ f.write(b"")
46
+ duration = 0.0
47
+ else:
48
+ # If audio or video missing, create a placeholder
49
+ with open(out_path, "wb") as f:
50
+ f.write(b"")
51
+ duration = 0.0
52
+
53
+ meta = {
54
+ "video_src": str(video_path),
55
+ "music_src": str(music_path),
56
+ "assembled_at": time.time() - t0
57
+ }
58
+ return {"final_path": str(out_path), "duration": duration, "meta": meta}
core/image_generator.py ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # core/image_generator.py
2
+ import os
3
+ import torch
4
+ from diffusers import StableDiffusionXLPipeline
5
+ from huggingface_hub import hf_hub_download
6
+ from pathlib import Path
7
+ from typing import List
8
+
9
+ # ---------------- MODEL CONFIG ----------------
10
+ MODEL_REPO = "SG161222/RealVisXL_V4.0"
11
+ MODEL_FILENAME = "realvisxlV40_v40LightningBakedvae.safetensors"
12
+ MODEL_DIR = Path("/tmp/models/realvisxl_v4")
13
+ os.makedirs(MODEL_DIR, exist_ok=True)
14
+
15
+ # ---------------- MODEL DOWNLOAD ----------------
16
+ def download_model() -> Path:
17
+ """
18
+ Downloads RealVisXL V4.0 model if not present.
19
+ Returns the local model path.
20
+ """
21
+ model_path = MODEL_DIR / MODEL_FILENAME
22
+ if not model_path.exists():
23
+ print("[ImageGen] Downloading RealVisXL V4.0 model...")
24
+ model_path = hf_hub_download(
25
+ repo_id=MODEL_REPO,
26
+ filename=MODEL_FILENAME,
27
+ local_dir=str(MODEL_DIR),
28
+ force_download=False,
29
+ )
30
+ print(f"[ImageGen] Model downloaded to: {model_path}")
31
+ else:
32
+ print("[ImageGen] Model already exists. Skipping download.")
33
+ return model_path
34
+
35
+ # ---------------- PIPELINE LOAD ----------------
36
+ def load_pipeline() -> StableDiffusionXLPipeline:
37
+ """
38
+ Loads the RealVisXL V4.0 model for image generation.
39
+ """
40
+ model_path = download_model()
41
+ print("[ImageGen] Loading model into pipeline...")
42
+ pipe = StableDiffusionXLPipeline.from_single_file(
43
+ str(model_path),
44
+ torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32,
45
+ )
46
+ if torch.cuda.is_available():
47
+ pipe.to("cuda")
48
+ print("[ImageGen] Model ready.")
49
+ return pipe
50
+
51
+ # ---------------- GLOBAL PIPELINE CACHE ----------------
52
+ pipe: StableDiffusionXLPipeline | None = None
53
+
54
+ # ---------------- IMAGE GENERATION ----------------
55
+ def generate_images(prompt: str, seed: int = None, num_images: int = 3) -> List:
56
+ """
57
+ Generates high-quality images using RealVisXL V4.0.
58
+ Supports deterministic generation using a seed.
59
+
60
+ Args:
61
+ prompt (str): Text prompt for image generation.
62
+ seed (int, optional): Seed for deterministic generation.
63
+ num_images (int): Number of images to generate.
64
+
65
+ Returns:
66
+ List: Generated PIL images.
67
+ """
68
+ global pipe
69
+ if pipe is None:
70
+ pipe = load_pipeline()
71
+
72
+ print(f"[ImageGen] Generating {num_images} image(s) for prompt: '{prompt}' with seed={seed}")
73
+ images = []
74
+
75
+ for i in range(num_images):
76
+ generator = None
77
+ if seed is not None:
78
+ device = "cuda" if torch.cuda.is_available() else "cpu"
79
+ generator = torch.Generator(device).manual_seed(seed + i) # slightly vary keyframes
80
+
81
+ result = pipe(prompt, num_inference_steps=30, generator=generator).images[0]
82
+ images.append(result)
83
+
84
+ print(f"[ImageGen] Generated {len(images)} images successfully.")
85
+ return images
core/music_generator.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # core/music_generator.py
2
+ """
3
+ Music generator stub. You can plug in a TTS or music model (Coqui, MusicLM, etc.)
4
+ Returns dict: {"music_path": ..., "duration": ..., "model": ...}
5
+ """
6
+ import asyncio
7
+ from pathlib import Path
8
+ import time
9
+ from concurrent.futures import ThreadPoolExecutor
10
+ from config import OUTPUT_DIR
11
+ from typing import Dict
12
+
13
+
14
+
15
+ OUTPUT_DIR = Path(OUTPUT_DIR)
16
+ OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
17
+ _executor = ThreadPoolExecutor(max_workers=1)
18
+
19
+ def _sync_generate_music(idea: str, out_path: Path, bpm=100):
20
+ """
21
+ Replace with your real music/TTS generator.
22
+ For now this creates a placeholder file and metadata.
23
+ """
24
+ t0 = time.time()
25
+ out_path.parent.mkdir(parents=True, exist_ok=True)
26
+ with open(out_path, "wb") as f:
27
+ f.write(b"") # replace with real audio bytes
28
+ meta = {"prompt": idea, "bpm": bpm, "time_taken": time.time() - t0}
29
+ return {"music_path": str(out_path), "duration": 3.0, "meta": meta}
30
+
31
+ async def generate_music(idea: str) -> Dict:
32
+ loop = asyncio.get_event_loop()
33
+ out_path = OUTPUT_DIR / f"music_{int(time.time()*1000)}.wav"
34
+ result = await loop.run_in_executor(_executor, _sync_generate_music, idea, out_path)
35
+ return result
core/script_gen.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # # script_gen.py
2
+ # import os
3
+ # import asyncio
4
+ # import httpx
5
+ # import logging
6
+ # from dotenv import load_dotenv
7
+
8
+ # dotenv_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), '.env')
9
+ # load_dotenv(dotenv_path)
10
+ # logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
11
+
12
+ # OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY")
13
+ # MODEL_NAME = "deepseek/deepseek-r1-distill-llama-70b:free"
14
+ # OPENROUTER_URL = f"https://openrouter.ai/api/v1/chat/completions"
15
+
16
+ # async def generate_script(idea: str) -> str:
17
+ # """
18
+ # Generate a highly interactive ad script from user idea.
19
+ # Includes detailed expressions, actions, and minute details.
20
+ # """
21
+ # prompt = f"""
22
+ # You are a professional ad writer.
23
+ # Take this idea and generate a fun, interactive, and detailed ad script.
24
+ # Include **minute expressions, subtle actions, emotions, and character reactions**.
25
+ # Idea: {idea}
26
+ # The script should be ready for storyboard creation.
27
+ # """
28
+
29
+ # headers = {"Authorization": f"Bearer {OPENROUTER_API_KEY}"}
30
+ # payload = {
31
+ # "model": MODEL_NAME,
32
+ # "messages": [{"role": "user", "content": prompt}],
33
+ # "temperature": 0.8,
34
+ # "max_tokens": 1200
35
+ # }
36
+
37
+ # async with httpx.AsyncClient(timeout=120) as client:
38
+ # response = await client.post(OPENROUTER_URL, json=payload, headers=headers)
39
+ # response.raise_for_status()
40
+ # data = response.json()
41
+
42
+ # script = data["choices"][0]["message"]["content"]
43
+ # logging.info("Script generated successfully")
44
+ # return script
45
+
46
+
47
+
48
+ # script_gen.py
49
+ import os
50
+ import asyncio
51
+ import httpx
52
+ import logging
53
+ from dotenv import load_dotenv
54
+
55
+ dotenv_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), '.env')
56
+ load_dotenv(dotenv_path)
57
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
58
+
59
+ OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY")
60
+ MODEL_NAME = "deepseek/deepseek-r1-distill-llama-70b:free"
61
+ OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions"
62
+
63
+ async def generate_script(idea: str) -> str:
64
+ """
65
+ Generate a short, funny, no-dialogue ad script based on a user idea.
66
+ The script should be written in plain text β€” simple narrative form,
67
+ no markdown, no camera angles, and no stage directions.
68
+ Just like:
69
+ A guy sits in a quiet library. He slowly opens a packet of Lay’s...
70
+ """
71
+ prompt = f"""
72
+ You are a creative ad script writer.
73
+ Write a short, funny, no-dialogue ad script from this idea: "{idea}".
74
+ Keep it simple and cinematic.
75
+ Use plain text only, with minimal expressions.
76
+ No markdown, no numbering, no headings, no scene titles.
77
+ Just write it like a mini story in clean text form.
78
+ """
79
+
80
+ headers = {"Authorization": f"Bearer {OPENROUTER_API_KEY}"}
81
+ payload = {
82
+ "model": MODEL_NAME,
83
+ "messages": [{"role": "user", "content": prompt}],
84
+ "temperature": 0.8,
85
+ "max_tokens": 600
86
+ }
87
+
88
+ async with httpx.AsyncClient(timeout=120) as client:
89
+ response = await client.post(OPENROUTER_URL, json=payload, headers=headers)
90
+ response.raise_for_status()
91
+ data = response.json()
92
+
93
+ script = data["choices"][0]["message"]["content"].strip()
94
+ logging.info("Simple script generated successfully")
95
+ return script
96
+
97
+ # Example usage (for testing)
98
+ # asyncio.run(generate_script("Lay’s funny ad in a library with no dialogues"))
core/script_generator.py ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # core/script_generator.py
2
+ import asyncio
3
+ import uuid
4
+ from typing import List, Dict
5
+ from config import OPENROUTER_API_KEY
6
+ from core.seed_manager import SeedManager
7
+ import httpx
8
+ import json
9
+
10
+ # Initialize seed manager
11
+ seed_manager = SeedManager()
12
+
13
+ # ---------------- OPENROUTER LLM CALL ----------------
14
+ async def _call_openrouter_llm(prompt: str) -> str:
15
+ """
16
+ Calls OpenRouter LLM to generate proposed video script.
17
+ Returns the raw text script.
18
+ """
19
+ url = "https://api.openrouter.ai/v1/chat/completions"
20
+ headers = {"Authorization": f"Bearer {OPENROUTER_API_KEY}"}
21
+ payload = {
22
+ "model": "gpt-4.1-mini", # powerful and suitable for script generation
23
+ "messages": [
24
+ {"role": "system", "content": "You are a professional creative video script writer."},
25
+ {"role": "user", "content": prompt}
26
+ ],
27
+ "max_tokens": 1500,
28
+ "temperature": 0.7
29
+ }
30
+
31
+ async with httpx.AsyncClient(timeout=60) as client:
32
+ response = await client.post(url, json=payload, headers=headers)
33
+ response.raise_for_status()
34
+ data = response.json()
35
+ # OpenRouter returns message content in choices[0].message.content
36
+ return data["choices"][0]["message"]["content"]
37
+
38
+ # ---------------- SCRIPT PROCESSING ----------------
39
+ def parse_script_to_scenes(script_text: str) -> List[Dict]:
40
+ """
41
+ Converts a script text into scene + keyframe JSON.
42
+ Each scene may have multiple keyframes.
43
+ Assigns unique scene_ids and seeds.
44
+ """
45
+ scenes_json = []
46
+ scene_counter = 1
47
+ keyframe_counter = 1
48
+
49
+ lines = [line.strip() for line in script_text.split("\n") if line.strip()]
50
+ for line in lines:
51
+ # Generate a unique scene_id and seed for this scene
52
+ scene_id = scene_counter
53
+ seed = seed_manager.generate_seed(scene_id)
54
+
55
+ # We assume each line is a keyframe
56
+ scenes_json.append({
57
+ "scene": scene_counter,
58
+ "scene_id": scene_id,
59
+ "keyframe_number": keyframe_counter,
60
+ "description": line,
61
+ "camera": "default", # can be improved later
62
+ "seed": seed
63
+ })
64
+
65
+ keyframe_counter += 1
66
+ scene_counter += 1
67
+
68
+ return scenes_json
69
+
70
+ # ---------------- MAIN FUNCTION ----------------
71
+ async def generate_script_async(idea: str, user_confirmed: bool = True) -> List[Dict]:
72
+ """
73
+ Full pipeline for script generation:
74
+ 1. Generates proposed script from LLM
75
+ 2. Waits for user confirmation
76
+ 3. Converts confirmed script into scene + keyframe JSON
77
+ """
78
+ prompt = f"Create a professional video script for: {idea}. Write each scene in one line."
79
+ raw_script = await _call_openrouter_llm(prompt)
80
+
81
+ # Here you can integrate actual user confirmation in your frontend
82
+ if not user_confirmed:
83
+ return [{"proposed_script": raw_script}]
84
+
85
+ # Convert approved script into structured scene/keyframe JSON
86
+ scenes = parse_script_to_scenes(raw_script)
87
+ return scenes
88
+
89
+ def generate_script(idea: str, user_confirmed: bool = True) -> List[Dict]:
90
+ """
91
+ Synchronous wrapper for pipeline integration.
92
+ """
93
+ return asyncio.get_event_loop().run_until_complete(
94
+ generate_script_async(idea, user_confirmed)
95
+ )
core/seed_manager.py ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # core/seed_manager.py
2
+ import json
3
+ import random
4
+ import threading
5
+ from pathlib import Path
6
+
7
+ DATA_DIR = Path(__file__).resolve().parent.parent / "data"
8
+ SEED_FILE = DATA_DIR / "seeds.json"
9
+ SEED_FILE.parent.mkdir(parents=True, exist_ok=True)
10
+
11
+ _lock = threading.Lock()
12
+
13
+ class SeedManager:
14
+ """
15
+ Persistent and thread-safe seed manager for reproducible image/video generation.
16
+ Maps: scene_id (str/int) -> seed (int)
17
+ """
18
+
19
+ def __init__(self, path: Path = SEED_FILE):
20
+ self.path = path
21
+ self._load()
22
+
23
+ def _load(self):
24
+ """Load seeds from disk safely."""
25
+ with _lock:
26
+ if self.path.exists():
27
+ try:
28
+ with open(self.path, "r", encoding="utf-8") as f:
29
+ self._store = json.load(f)
30
+ except Exception:
31
+ self._store = {}
32
+ else:
33
+ self._store = {}
34
+
35
+ def _save(self):
36
+ """Save seeds to disk safely."""
37
+ with _lock:
38
+ with open(self.path, "w", encoding="utf-8") as f:
39
+ json.dump(self._store, f, indent=2)
40
+
41
+ def get_seed(self, scene_id: str | int) -> int | None:
42
+ """Return existing seed for a scene_id or None if not set."""
43
+ return self._store.get(str(scene_id))
44
+
45
+ def store_seed(self, scene_id: str | int, seed: int):
46
+ """Store a seed persistently for a given scene_id."""
47
+ self._store[str(scene_id)] = int(seed)
48
+ self._save()
49
+
50
+ def ensure_seed(self, scene_id: str | int) -> int:
51
+ """
52
+ Return existing seed or generate/store a new one.
53
+ Guarantees deterministic reproducible seed for the same scene_id.
54
+ """
55
+ s = self.get_seed(scene_id)
56
+ if s is None:
57
+ s = random.randint(0, 2**31 - 1)
58
+ self.store_seed(scene_id, s)
59
+ return s
60
+
61
+ def reset(self):
62
+ """Clear all seeds (useful for testing or new projects)."""
63
+ with _lock:
64
+ self._store = {}
65
+ self._save()
core/story_script.py ADDED
@@ -0,0 +1,593 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # # # story_script.py
2
+ # # import asyncio
3
+ # # import json
4
+ # # import logging
5
+ # # import random
6
+
7
+ # # logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
8
+
9
+ # # async def generate_story(script: str) -> dict:
10
+ # # """
11
+ # # Convert the ad script into a structured storyboard JSON format:
12
+ # # - Characters
13
+ # # - Scenes with keyframes, camera instructions, and music
14
+ # # """
15
+ # # # Sample character extraction (for simplicity, can be improved)
16
+ # # characters = [{"name": name, "seed": idx+1} for idx, name in enumerate(["MainGuy", "Friend1", "Friend2"])]
17
+
18
+ # # # Split script into lines and create scenes (basic heuristic, can be improved)
19
+ # # lines = [line.strip() for line in script.split("\n") if line.strip()]
20
+ # # scenes = {}
21
+ # # for idx, line in enumerate(lines, start=1):
22
+ # # char = characters[idx % len(characters)]["name"]
23
+ # # seed = characters[idx % len(characters)]["seed"]
24
+ # # scenes[f"scene{idx}"] = {
25
+ # # "character": char,
26
+ # # "scene": line,
27
+ # # "keyframes": [
28
+ # # {
29
+ # # "seed": seed,
30
+ # # "keyframe1": f"{char} in action based on script line: '{line[:40]}...'",
31
+ # # "keyframe2": f"{char} expressive close-up reacting to: '{line[:40]}...'"
32
+ # # }
33
+ # # ],
34
+ # # "camera": "Medium shot with dynamic zoom-ins",
35
+ # # "music": "Appropriate upbeat or dramatic tune based on action"
36
+ # # }
37
+
38
+ # # storyboard = {
39
+ # # "characters": characters,
40
+ # # **scenes
41
+ # # }
42
+
43
+ # # logging.info("Story script generated successfully")
44
+ # # return storyboard
45
+
46
+
47
+ # # # best
48
+ # # # story_script.py
49
+ # # import os
50
+ # # import asyncio
51
+ # # import httpx
52
+ # # import logging
53
+ # # from dotenv import load_dotenv
54
+ # # from dotenv import load_dotenv
55
+
56
+ # # dotenv_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), '.env')
57
+ # # load_dotenv(dotenv_path)
58
+
59
+ # # load_dotenv()
60
+ # # logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
61
+
62
+ # # OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY")
63
+ # # MODEL_NAME = "deepseek/deepseek-r1-distill-llama-70b:free"
64
+ # # OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions"
65
+
66
+ # # async def generate_story(script: str) -> dict:
67
+ # # """
68
+ # # Convert the ad script into a structured storyboard JSON using AI.
69
+ # # The JSON format includes:
70
+ # # - characters (with seeds)
71
+ # # - scenes (character, scene description, keyframes, camera, music)
72
+ # # """
73
+ # # prompt = f"""
74
+ # # You are a professional ad storyboard generator.
75
+ # # Take this ad script and convert it into a **storyboard JSON**.
76
+ # # Follow this format exactly:
77
+
78
+ # # ADD {{
79
+ # # "characters": [
80
+ # # {{"name": "MainGuy", "seed": 1}},
81
+ # # {{"name": "Friend1", "seed": 2}},
82
+ # # {{"name": "Friend2", "seed": 3}}
83
+ # # ],
84
+ # # "scene1": {{
85
+ # # "character": "MainGuy",
86
+ # # "scene": "Description of the scene",
87
+ # # "keyframes": [
88
+ # # {{
89
+ # # "seed": 1,
90
+ # # "keyframe1": "First keyframe description",
91
+ # # "keyframe2": "Second keyframe description"
92
+ # # }}
93
+ # # ],
94
+ # # "camera": "Camera instructions",
95
+ # # "music": "Music instructions"
96
+ # # }}
97
+ # # ...
98
+ # # }}
99
+
100
+ # # Ensure:
101
+ # # - Use the **script lines as scenes**.
102
+ # # - Assign characters logically to actions.
103
+ # # - Provide **keyframes, camera, and music**.
104
+ # # - Return **valid JSON only**, no extra text.
105
+ # # Script:
106
+ # # \"\"\"{script}\"\"\"
107
+ # # """
108
+
109
+ # # headers = {"Authorization": f"Bearer {OPENROUTER_API_KEY}"}
110
+ # # payload = {
111
+ # # "model": MODEL_NAME,
112
+ # # "messages": [{"role": "user", "content": prompt}],
113
+ # # "temperature": 0.8,
114
+ # # "max_tokens": 1200
115
+ # # }
116
+
117
+ # # async with httpx.AsyncClient(timeout=120) as client:
118
+ # # response = await client.post(OPENROUTER_URL, json=payload, headers=headers)
119
+ # # response.raise_for_status()
120
+ # # data = response.json()
121
+
122
+ # # # AI returns the JSON as a string
123
+ # # story_json_str = data["choices"][0]["message"]["content"]
124
+
125
+ # # # Remove possible extra text before/after JSON (some AI outputs might wrap with "ADD {...}")
126
+ # # if story_json_str.startswith("ADD"):
127
+ # # story_json_str = story_json_str[story_json_str.find("{"):]
128
+
129
+ # # # Convert string to dict
130
+ # # try:
131
+ # # story_dict = eval(story_json_str) # safe because AI returns JSON-like dict
132
+ # # except Exception as e:
133
+ # # logging.error(f"Failed to parse story JSON: {e}")
134
+ # # story_dict = {}
135
+
136
+ # # logging.info("Story script generated successfully using AI")
137
+ # # return story_dict
138
+
139
+
140
+
141
+
142
+ # # import os
143
+ # # import asyncio
144
+ # # import httpx
145
+ # # import logging
146
+ # # import json
147
+ # # from dotenv import load_dotenv
148
+
149
+ # # dotenv_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), '.env')
150
+ # # load_dotenv(dotenv_path)
151
+
152
+ # # logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
153
+
154
+ # # OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY")
155
+ # # MODEL_NAME = "deepseek/deepseek-r1-distill-llama-70b:free"
156
+ # # OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions"
157
+
158
+ # # async def generate_story(script: str) -> dict:
159
+ # # """
160
+ # # Convert the ad script into a structured storyboard JSON using AI.
161
+ # # The JSON format includes:
162
+ # # - characters (with seeds)
163
+ # # - scenes (character, scene description, keyframes, camera, music)
164
+ # # """
165
+ # # prompt = f"""
166
+ # # You are a professional ad storyboard generator.
167
+ # # Take this ad script and convert it into a **storyboard JSON**.
168
+ # # Follow this format exactly:
169
+
170
+ # # ADD {{
171
+ # # "characters": [
172
+ # # {{"name": "MainGuy", "seed": 1}},
173
+ # # {{"name": "Friend1", "seed": 2}},
174
+ # # {{"name": "Friend2", "seed": 3}}
175
+ # # ],
176
+ # # "scene1": {{
177
+ # # "character": "MainGuy",
178
+ # # "scene": "Description of the scene",
179
+ # # "keyframes": [
180
+ # # {{
181
+ # # "seed": 1,
182
+ # # "keyframe1": "First keyframe description",
183
+ # # "keyframe2": "Second keyframe description"
184
+ # # }}
185
+ # # ],
186
+ # # "camera": "Camera instructions",
187
+ # # "music": "Music instructions"
188
+ # # }}
189
+ # # ...
190
+ # # }}
191
+
192
+ # # Ensure:
193
+ # # - Use the **script lines as scenes**.
194
+ # # - Assign characters logically to actions.
195
+ # # - Provide **keyframes, camera, and music**.
196
+ # # - Return **valid JSON only**, no extra text, no markdown, no ``` fences.
197
+ # # Script:
198
+ # # \"\"\"{script}\"\"\"
199
+ # # """
200
+
201
+ # # headers = {"Authorization": f"Bearer {OPENROUTER_API_KEY}"}
202
+ # # payload = {
203
+ # # "model": MODEL_NAME,
204
+ # # "messages": [{"role": "user", "content": prompt}],
205
+ # # "temperature": 0.8,
206
+ # # "max_tokens": 1500
207
+ # # }
208
+
209
+ # # async with httpx.AsyncClient(timeout=120) as client:
210
+ # # response = await client.post(OPENROUTER_URL, json=payload, headers=headers)
211
+ # # response.raise_for_status()
212
+ # # data = response.json()
213
+
214
+ # # story_json_str = data["choices"][0]["message"]["content"]
215
+
216
+ # # # Clean unwanted wrappers
217
+ # # story_json_str = story_json_str.strip()
218
+ # # if story_json_str.startswith("ADD"):
219
+ # # story_json_str = story_json_str[story_json_str.find("{"):]
220
+
221
+ # # # Remove markdown fences
222
+ # # story_json_str = story_json_str.replace("```json", "").replace("```", "").strip()
223
+
224
+ # # # Parse safely
225
+ # # try:
226
+ # # story_dict = json.loads(story_json_str)
227
+ # # except json.JSONDecodeError as e:
228
+ # # logging.error(f"❌ Failed to parse story JSON properly: {e}")
229
+ # # story_dict = {"raw_output": story_json_str}
230
+
231
+ # # logging.info("Story script generated successfully using AI")
232
+ # # return story_dict
233
+
234
+
235
+ # import os
236
+ # import asyncio
237
+ # import httpx
238
+ # import logging
239
+ # import json
240
+ # import re
241
+ # from dotenv import load_dotenv
242
+
243
+ # dotenv_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), '.env')
244
+ # load_dotenv(dotenv_path)
245
+
246
+ # logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
247
+
248
+ # OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY")
249
+ # MODEL_NAME = "deepseek/deepseek-r1-distill-llama-70b:free"
250
+ # OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions"
251
+
252
+
253
+ # async def generate_story(script: str) -> dict:
254
+ # """
255
+ # Convert the ad script into a structured storyboard JSON using AI.
256
+ # Returns:
257
+ # {
258
+ # "raw_output": "<original AI string>",
259
+ # "parsed_output": <dict | None>
260
+ # }
261
+ # """
262
+ # prompt = f"""
263
+ # You are a professional ad storyboard generator.
264
+ # Take this ad script and convert it into a **storyboard JSON**.
265
+ # Follow this format exactly:
266
+
267
+ # ADD {{
268
+ # "characters": [
269
+ # {{"name": "MainGuy-", "seed": 1}},
270
+ # {{"name": "Friend1", "seed": 2}},
271
+ # {{"name": "Friend2", "seed": 3}}
272
+ # ],
273
+ # "scene1": {{
274
+ # "character": "MainGuy",
275
+ # "scene": "Description of the scene",
276
+ # "keyframes": [
277
+ # {{
278
+ # "seed": 1,
279
+ # "keyframe1": "First keyframe description",
280
+ # "keyframe2": "Second keyframe description"
281
+ # }}
282
+ # ],
283
+ # "camera": "Camera instructions",
284
+ # "music": "Music instructions"
285
+ # }}
286
+ # }}
287
+
288
+ # Ensure:
289
+ # - Each scene corresponds to a line in the script.
290
+ # - Assign logical characters.
291
+ # - Return only valid JSON (no markdown or explanations).
292
+ # Script:
293
+ # \"\"\"{script}\"\"\"
294
+ # """
295
+
296
+ # headers = {"Authorization": f"Bearer {OPENROUTER_API_KEY}"}
297
+ # payload = {
298
+ # "model": MODEL_NAME,
299
+ # "messages": [{"role": "user", "content": prompt}],
300
+ # "temperature": 0.8,
301
+ # "max_tokens": 1500
302
+ # }
303
+
304
+ # async with httpx.AsyncClient(timeout=120) as client:
305
+ # response = await client.post(OPENROUTER_URL, json=payload, headers=headers)
306
+ # response.raise_for_status()
307
+ # data = response.json()
308
+
309
+ # story_json_str = data["choices"][0]["message"]["content"].strip()
310
+ # raw_output = story_json_str
311
+
312
+ # # --- Cleaning Stage ---
313
+ # story_json_str = story_json_str.strip()
314
+ # if story_json_str.startswith("ADD"):
315
+ # story_json_str = story_json_str[story_json_str.find("{"):]
316
+
317
+ # # Remove markdown code fences and artifacts
318
+ # story_json_str = story_json_str.replace("```json", "").replace("```", "").strip()
319
+
320
+ # # Remove unwanted triple quotes, trailing commas, and unescaped slashes
321
+ # story_json_str = re.sub(r',\s*}', '}', story_json_str)
322
+ # story_json_str = re.sub(r',\s*\]', ']', story_json_str)
323
+ # story_json_str = story_json_str.replace('\\"', '"').replace("\\'", "'")
324
+
325
+ # parsed_story = None
326
+
327
+ # # --- Parsing Stage ---
328
+ # try:
329
+ # parsed_story = json.loads(story_json_str)
330
+ # except json.JSONDecodeError:
331
+ # try:
332
+ # # Handle double-encoded or escaped JSON
333
+ # cleaned_str = bytes(story_json_str, "utf-8").decode("unicode_escape")
334
+ # parsed_story = json.loads(cleaned_str)
335
+ # except Exception as e:
336
+ # logging.error(f"❌ JSON parse failed after cleaning: {e}")
337
+ # parsed_story = None
338
+
339
+ # logging.info("βœ… Storyboard generation completed")
340
+ # return parsed_story
341
+
342
+
343
+ ##best best
344
+ # import os
345
+ # import asyncio
346
+ # import httpx
347
+ # import logging
348
+ # import json
349
+ # import re
350
+ # from dotenv import load_dotenv
351
+
352
+ # dotenv_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), '.env')
353
+ # load_dotenv(dotenv_path)
354
+
355
+ # logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
356
+
357
+ # OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY")
358
+ # MODEL_NAME = "deepseek/deepseek-r1-distill-llama-70b:free"
359
+ # OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions"
360
+
361
+
362
+ # async def generate_story(script: str) -> dict:
363
+ # """
364
+ # Convert the ad script into a structured storyboard JSON using AI.
365
+ # Always returns:
366
+ # {
367
+ # "raw_output": "<original text from AI>",
368
+ # "parsed_output": <dict or None>
369
+ # }
370
+ # """
371
+ # prompt = f"""
372
+ # You are a professional ad storyboard generator.
373
+ # Convert this ad script into a **storyboard JSON** only β€” no extra text.
374
+
375
+ # Format example:
376
+ # {{
377
+ # "characters": [
378
+ # {{"name": "MainGuy","Description":"complete decsripiton of the character", "seed": 1}},
379
+ # {{"name": "Dog","Description":"complete decsripiton of the character", "seed": 2}}
380
+ # ],
381
+ # "scene1": {{
382
+ # "character": "MainGuy",
383
+ # "scene": "Man wakes up late and rushes outside",
384
+ # "keyframes": [
385
+ # {{"seed": 1, "keyframe1": "Man stepping in puddle", "keyframe2": "Reaction close-up"}}
386
+ # ],
387
+ # "camera": "Medium shot with soft lighting",
388
+ # "music": "Playful upbeat tune"
389
+ # }}
390
+ # }}
391
+ # Script:
392
+ # \"\"\"{script}\"\"\"
393
+ # Return **only valid JSON**, no markdown or commentary.
394
+ # """
395
+
396
+ # headers = {"Authorization": f"Bearer {OPENROUTER_API_KEY}"}
397
+ # payload = {
398
+ # "model": MODEL_NAME,
399
+ # "messages": [{"role": "user", "content": prompt}],
400
+ # "temperature": 0.7,
401
+ # "max_tokens": 1500
402
+ # }
403
+
404
+ # async with httpx.AsyncClient(timeout=120) as client:
405
+ # response = await client.post(OPENROUTER_URL, json=payload, headers=headers)
406
+ # response.raise_for_status()
407
+ # data = response.json()
408
+
409
+ # story_json_str = data["choices"][0]["message"]["content"].strip()
410
+ # raw_output = story_json_str
411
+
412
+ # # --- Clean the output ---
413
+ # story_json_str = re.sub(r"^ADD\s*", "", story_json_str)
414
+ # story_json_str = story_json_str.replace("```json", "").replace("```", "")
415
+ # story_json_str = story_json_str.replace("β€œ", "\"").replace("”", "\"").replace("’", "'")
416
+ # story_json_str = re.sub(r",\s*([}\]])", r"\1", story_json_str) # remove trailing commas
417
+ # story_json_str = story_json_str.strip()
418
+
419
+ # # --- Parse the JSON safely ---
420
+ # parsed_story = None
421
+ # try:
422
+ # parsed_story = json.loads(story_json_str)
423
+ # except json.JSONDecodeError as e:
424
+ # logging.warning(f"JSON parse failed: {e}")
425
+ # try:
426
+ # # try to find JSON substring in case AI wrapped it with text
427
+ # match = re.search(r"\{.*\}", story_json_str, re.DOTALL)
428
+ # if match:
429
+ # parsed_story = json.loads(match.group(0))
430
+ # except Exception as e2:
431
+ # logging.error(f"Final parsing failed: {e2}")
432
+ # parsed_story = None
433
+
434
+ # logging.info("βœ… Storyboard generation completed")
435
+ # return parsed_story
436
+
437
+
438
+
439
+
440
+
441
+ import os
442
+ import asyncio
443
+ import httpx
444
+ import logging
445
+ import json
446
+ import re
447
+ from dotenv import load_dotenv
448
+
449
+ # --- Load environment variables ---
450
+ dotenv_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), '.env')
451
+ load_dotenv(dotenv_path)
452
+
453
+ # --- Configure logging ---
454
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
455
+
456
+ # --- API Constants ---
457
+ OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY")
458
+ MODEL_NAME = "deepseek/deepseek-r1-distill-llama-70b:free"
459
+ OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions"
460
+
461
+
462
+ async def generate_story(script: str) -> dict:
463
+ """
464
+ Converts ad script into a structured storyboard JSON.
465
+ Each scene = only one action.
466
+ Reuses character seeds for consistency.
467
+ Always returns a non-null dictionary.
468
+ """
469
+
470
+ prompt = f"""
471
+ You are a professional storyboard generator for AI video production.
472
+
473
+ Convert the given ad script into a JSON storyboard format.
474
+ Each scene must represent only ONE clear action or emotional beat.
475
+ Do not include any explanations, markdown, or text outside JSON.
476
+
477
+ ### STRICT JSON FORMAT:
478
+ {{
479
+ "characters": [
480
+ {{"name": "Man","description":"average build, brown hair, casual outfit","seed":1}},
481
+ {{"name": "Librarian","description":"stern woman, cat-eye glasses, neat bun","seed":2}}
482
+ ],
483
+ "scene1": {{
484
+ "character": "Man",
485
+ "scene": "Man enters the library holding a bag of potato chips",
486
+ "keyframes": [
487
+ {{
488
+ "seed": 1,
489
+ "keyframe1": "Man walking into a quiet library, holding a bag of potato chips, warm sunlight from windows, calm mood",
490
+ "keyframe2": "Side shot of man sitting down at a wooden table, casual expression, sunlight glows behind him"
491
+ }}
492
+ ],
493
+ "camera": "Medium wide shot with soft natural lighting",
494
+ "music": "Gentle ambient tune"
495
+ }}
496
+ }}
497
+
498
+ Rules:
499
+ - Each scene = ONE clear action only.
500
+ - Use each character’s 'seed' consistently across scenes.
501
+ - Each keyframe describes two cinematic angles of that action.
502
+ - Keep descriptions detailed but realistic.
503
+ - Return valid JSON only β€” no extra text, comments, or markdown.
504
+
505
+ Script:
506
+ \"\"\"{script}\"\"\"
507
+ """
508
+
509
+ headers = {"Authorization": f"Bearer {OPENROUTER_API_KEY}"}
510
+ payload = {
511
+ "model": MODEL_NAME,
512
+ "messages": [{"role": "user", "content": prompt}],
513
+ "temperature": 0.7,
514
+ "max_tokens": 1800
515
+ }
516
+
517
+ async with httpx.AsyncClient(timeout=180) as client:
518
+ try:
519
+ response = await client.post(OPENROUTER_URL, json=payload, headers=headers)
520
+ response.raise_for_status()
521
+ data = response.json()
522
+ except Exception as e:
523
+ logging.error(f"API request failed: {e}")
524
+ return {"error": "API request failed", "details": str(e)}
525
+
526
+ story_json_str = data["choices"][0]["message"]["content"].strip()
527
+
528
+ # --- Clean the model output ---
529
+ story_json_str = re.sub(r"```(?:json)?", "", story_json_str)
530
+ story_json_str = story_json_str.replace("β€œ", "\"").replace("”", "\"").replace("’", "'")
531
+ story_json_str = re.sub(r",\s*([}\]])", r"\1", story_json_str).strip()
532
+
533
+ parsed_story = None
534
+ try:
535
+ parsed_story = json.loads(story_json_str)
536
+ except json.JSONDecodeError as e:
537
+ logging.warning(f"Initial JSON parse failed: {e}")
538
+ match = re.search(r"\{.*\}", story_json_str, re.DOTALL)
539
+ if match:
540
+ try:
541
+ parsed_story = json.loads(match.group(0))
542
+ except Exception as e2:
543
+ logging.error(f"Fallback parse failed: {e2}")
544
+
545
+ # --- Final fallback: minimal structure ---
546
+ if not parsed_story:
547
+ logging.warning("Model output invalid, generating fallback JSON.")
548
+ parsed_story = {
549
+ "characters": [],
550
+ "scene1": {
551
+ "character": "Unknown",
552
+ "scene": "Failed to parse script properly",
553
+ "keyframes": [
554
+ {
555
+ "seed": 0,
556
+ "keyframe1": "Generic placeholder image",
557
+ "keyframe2": "Generic placeholder image"
558
+ }
559
+ ],
560
+ "camera": "Static fallback frame",
561
+ "music": "None"
562
+ }
563
+ }
564
+
565
+ # --- Ensure consistent seeds & single-action scenes ---
566
+ characters = {c.get("name"): c for c in parsed_story.get("characters", [])}
567
+ for key, scene in parsed_story.items():
568
+ if not key.startswith("scene"):
569
+ continue
570
+
571
+ char_name = scene.get("character")
572
+ seed = characters.get(char_name, {}).get("seed", 0)
573
+ desc = characters.get(char_name, {}).get("description", "")
574
+
575
+ # Limit to one clear action
576
+ scene_text = scene.get("scene", "")
577
+ scene["scene"] = re.split(r"[,.] and |, then |;| but | while ", scene_text)[0].strip()
578
+
579
+ # Keyframe corrections
580
+ for kf in scene.get("keyframes", []):
581
+ kf["seed"] = seed
582
+ for i, k in enumerate(["keyframe1", "keyframe2"]):
583
+ if not kf.get(k):
584
+ angle = "wide shot" if i == 0 else "close-up"
585
+ kf[k] = (
586
+ f"{char_name} ({desc}) performing '{scene['scene']}', "
587
+ f"{angle}, cinematic tone, photorealistic lighting"
588
+ )
589
+
590
+ logging.info("βœ… Storyboard generated successfully with consistent single-action scenes.")
591
+ return parsed_story
592
+
593
+
core/video_generator.py ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import torch
3
+ from pathlib import Path
4
+ from huggingface_hub import hf_hub_download
5
+ from diffusers import AnimateDiffPipeline, MotionAdapter
6
+ from typing import List
7
+ from PIL import Image
8
+
9
+ # ---------------- MODEL CONFIG ----------------
10
+ MODEL_REPO = "ByteDance/AnimateDiff-Lightning"
11
+ MODEL_FILENAME = "animatediff_lightning_8step_comfyui.safetensors"
12
+ MODEL_DIR = Path("/tmp/models/animatediff_lightning")
13
+ os.makedirs(MODEL_DIR, exist_ok=True)
14
+
15
+ # ---------------- MODEL DOWNLOAD ----------------
16
+ def download_model() -> Path:
17
+ model_path = MODEL_DIR / MODEL_FILENAME
18
+ if not model_path.exists():
19
+ print("[VideoGen] Downloading AnimateDiff Lightning 8-step...")
20
+ model_path = hf_hub_download(
21
+ repo_id=MODEL_REPO,
22
+ filename=MODEL_FILENAME,
23
+ local_dir=str(MODEL_DIR),
24
+ force_download=False,
25
+ )
26
+ print(f"[VideoGen] Model downloaded to: {model_path}")
27
+ else:
28
+ print("[VideoGen] AnimateDiff model already exists.")
29
+ return model_path
30
+
31
+ # ---------------- PIPELINE LOAD ----------------
32
+ def load_pipeline() -> AnimateDiffPipeline:
33
+ model_path = download_model()
34
+ print("[VideoGen] Loading AnimateDiff pipeline...")
35
+
36
+ adapter = MotionAdapter.from_single_file(str(model_path))
37
+ pipe = AnimateDiffPipeline.from_pretrained(
38
+ "emilianJR/epiCRealism",
39
+ motion_adapter=adapter,
40
+ torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32,
41
+ )
42
+
43
+ if torch.cuda.is_available():
44
+ pipe.to("cuda")
45
+
46
+ print("[VideoGen] AnimateDiff ready.")
47
+ return pipe
48
+
49
+ # ---------------- GLOBAL PIPELINE CACHE ----------------
50
+ pipe: AnimateDiffPipeline | None = None
51
+
52
+ # ---------------- VIDEO GENERATION ----------------
53
+ def generate_video(
54
+ keyframe_images: List[Image.Image],
55
+ seed: int = None,
56
+ num_frames: int = 16
57
+ ) -> List[Image.Image]:
58
+ """
59
+ Generates a short video by interpolating between input keyframe images.
60
+
61
+ Args:
62
+ keyframe_images (List[PIL.Image]): List of PIL images representing keyframes.
63
+ seed (int, optional): Seed for deterministic generation.
64
+ num_frames (int): Total number of frames in the generated video.
65
+
66
+ Returns:
67
+ List[PIL.Image]: Interpolated video frames.
68
+ """
69
+ global pipe
70
+ if pipe is None:
71
+ pipe = load_pipeline()
72
+
73
+ if len(keyframe_images) < 2:
74
+ raise ValueError("At least 2 keyframe images are required to generate a video.")
75
+
76
+ print(f"[VideoGen] Generating video from {len(keyframe_images)} keyframes, {num_frames} frames, seed={seed}")
77
+
78
+ generator = None
79
+ if seed is not None:
80
+ device = "cuda" if torch.cuda.is_available() else "cpu"
81
+ generator = torch.Generator(device).manual_seed(seed)
82
+
83
+ # AnimateDiff expects init_images for interpolation between keyframes
84
+ video_frames = pipe(
85
+ init_images=keyframe_images,
86
+ num_frames=num_frames,
87
+ guidance_scale=1.0,
88
+ num_inference_steps=8,
89
+ generator=generator
90
+ ).frames
91
+
92
+ print("[VideoGen] Video generated successfully.")
93
+ return video_frames
pipeline/_init.py ADDED
File without changes
pipeline/pipeline.py ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # pipeline.py
2
+ import asyncio
3
+ import logging
4
+ from api.server import pending_confirmations # Access the confirmation events
5
+
6
+ # Import your modules
7
+ import core.script_gen as script_gen
8
+ import core.story_script as story_script
9
+ # import core.image_gen as image_gen
10
+ # import core.video_gen as video_gen
11
+ # import core.music_gen as music_gen
12
+ # import core.assemble as assemble
13
+
14
+ logging.basicConfig(
15
+ level=logging.INFO,
16
+ format="%(asctime)s [%(levelname)s] %(message)s"
17
+ )
18
+
19
+ async def run_pipeline(task: dict):
20
+ task_id = task["task_id"]
21
+ idea = task["idea"]
22
+
23
+ logging.info(f"[Pipeline] Starting script generation for task {task_id}")
24
+ script = await script_gen.generate_script(idea) # Async script generation
25
+
26
+ logging.info(f"[Pipeline] Waiting for user confirmation for task {task_id}")
27
+ # Wait for confirmation (manual or auto)
28
+ if task_id in pending_confirmations:
29
+ await pending_confirmations[task_id].wait()
30
+ else:
31
+ logging.info(f"[Pipeline] No pending confirmation found, auto-confirming task {task_id}")
32
+
33
+ logging.info(f"[Pipeline] Generating story script for task {task_id}")
34
+ story = await story_script.generate_story(script)
35
+ final_output = story # Placeholder for final output
36
+ # logging.info(f"[Pipeline] Generating images for task {task_id}")
37
+ # images = await image_gen.generate_images(story)
38
+
39
+ # logging.info(f"[Pipeline] Generating video for task {task_id}")
40
+ # video = await video_gen.generate_video(images)
41
+
42
+ # logging.info(f"[Pipeline] Generating music/audio for task {task_id}")
43
+ # audio = await music_gen.generate_music(story)
44
+
45
+ # logging.info(f"[Pipeline] Assembling final output for task {task_id}")
46
+ # final_output = await assemble.create_final(video, audio)
47
+
48
+ logging.info(f"[Pipeline] Task {task_id} completed. Output: {final_output}")
49
+ return final_output
services/__init__.py ADDED
File without changes
services/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (164 Bytes). View file
 
services/__pycache__/queue_manager.cpython-311.pyc ADDED
Binary file (5.08 kB). View file
 
services/queue_manager.py ADDED
@@ -0,0 +1,401 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # # services/queue_manager.py
2
+ # import os
3
+ # import uuid
4
+ # import asyncio
5
+ # import pickle
6
+ # from collections import deque
7
+ # from concurrent.futures import ThreadPoolExecutor
8
+ # from typing import Callable, Any
9
+
10
+ # # ---------------- CONFIG ----------------
11
+ # RAM_QUEUE_LIMIT = 10
12
+ # DISK_QUEUE_DIR = "task_queue_disk"
13
+ # os.makedirs(DISK_QUEUE_DIR, exist_ok=True)
14
+
15
+ # # ---------------- INTERNAL STORAGE ----------------
16
+ # ram_queue = deque()
17
+ # disk_queue_files = deque()
18
+ # futures = {}
19
+ # executor = ThreadPoolExecutor(max_workers=2) # Can process 2 tasks concurrently
20
+ # processing = False
21
+ # lock = asyncio.Lock()
22
+
23
+
24
+ # class QueueManager:
25
+ # def __init__(self):
26
+ # self.ram_queue = ram_queue
27
+ # self.disk_queue_files = disk_queue_files
28
+ # self.futures = futures
29
+ # self.executor = executor
30
+ # self.processing = False
31
+ # self.lock = lock
32
+
33
+ # async def _process_queue(self):
34
+ # """Process tasks one at a time in strict FIFO order."""
35
+ # async with self.lock:
36
+ # if self.processing:
37
+ # return
38
+ # self.processing = True
39
+
40
+ # try:
41
+ # while self.ram_queue or self.disk_queue_files:
42
+ # if not self.ram_queue and self.disk_queue_files:
43
+ # # Move one task from disk to RAM
44
+ # file_path = self.disk_queue_files.popleft()
45
+ # with open(file_path, "rb") as f:
46
+ # task_data = pickle.load(f)
47
+ # os.remove(file_path)
48
+ # self.ram_queue.append(task_data)
49
+ # print(f"[Queue] Loaded task from disk to RAM.")
50
+
51
+ # if not self.ram_queue:
52
+ # break
53
+
54
+ # task_id, func, args, kwargs = self.ram_queue.popleft()
55
+ # print(f"[Queue] Processing task {task_id}...")
56
+
57
+ # loop = asyncio.get_event_loop()
58
+ # # Run in executor to avoid blocking event loop
59
+ # result = await loop.run_in_executor(self.executor, func, *args, **kwargs)
60
+
61
+ # # Set result in future
62
+ # fut = self.futures.get(task_id)
63
+ # if fut and not fut.done():
64
+ # fut.set_result(result)
65
+ # print(f"[Queue] Task {task_id} completed.")
66
+
67
+ # finally:
68
+ # self.processing = False
69
+
70
+ # def enqueue_task(self, func: Callable, *args, **kwargs) -> str:
71
+ # """
72
+ # Add a task to the queue.
73
+ # func: Callable function to execute.
74
+ # args, kwargs: Arguments for the function.
75
+ # Returns a unique task_id.
76
+ # """
77
+ # task_id = str(uuid.uuid4())
78
+ # loop = asyncio.get_event_loop()
79
+ # fut = loop.create_future()
80
+ # self.futures[task_id] = fut
81
+
82
+ # task_data = (task_id, func, args, kwargs)
83
+
84
+ # if len(self.ram_queue) < RAM_QUEUE_LIMIT:
85
+ # self.ram_queue.append(task_data)
86
+ # print(f"[Queue] Task {task_id} added to RAM queue.")
87
+ # else:
88
+ # file_path = os.path.join(DISK_QUEUE_DIR, f"{task_id}.pkl")
89
+ # with open(file_path, "wb") as f:
90
+ # pickle.dump(task_data, f)
91
+ # self.disk_queue_files.append(file_path)
92
+ # print(f"[Queue] Task {task_id} saved to disk.")
93
+
94
+ # # Start processing
95
+ # loop.create_task(self._process_queue())
96
+ # return task_id
97
+
98
+ # def get_future(self, task_id: str):
99
+ # """Get the future object for a task_id."""
100
+ # return self.futures.get(task_id)
101
+
102
+ # def get_queue_status(self) -> dict:
103
+ # """Return current queue info."""
104
+ # total_q = len(self.ram_queue) + len(self.disk_queue_files)
105
+ # return {
106
+ # "status": "free" if total_q == 0 else "busy",
107
+ # "queue_length": total_q,
108
+ # "ram_queue": len(self.ram_queue),
109
+ # "disk_queue": len(self.disk_queue_files),
110
+ # "processing": self.processing
111
+ # }
112
+
113
+ # # # Singleton instance for the pipeline.pipeline to use
114
+ # # queue_manager = QueueManager()
115
+
116
+
117
+
118
+ # # best
119
+ # # queue.py
120
+ # import asyncio
121
+ # import uuid
122
+ # from typing import Dict, Any, Optional
123
+ # from enum import Enum
124
+
125
+ # # === Import all stage functions ===
126
+ # # queue_manager.py
127
+ # # queue_manager.py inside core/
128
+ # from core.script_gen import generate_script
129
+ # from core.story_script import generate_story
130
+ # # from core.image_gen import generate_images
131
+ # # from core.video_gen import generate_video
132
+ # # from core.music_gen import generate_music
133
+ # # from core.assemble import assemble_final_video
134
+
135
+
136
+
137
+ # # -------------------------------------------------------------
138
+ # # ENUMS AND GLOBALS
139
+ # # -------------------------------------------------------------
140
+ # class TaskStatus(str, Enum):
141
+ # PENDING = "pending"
142
+ # RUNNING = "running"
143
+ # WAITING_CONFIRMATION = "waiting_for_confirmation"
144
+ # CONFIRMED = "confirmed"
145
+ # COMPLETED = "completed"
146
+ # FAILED = "failed"
147
+
148
+
149
+ # # All active tasks are tracked here (in-memory)
150
+ # tasks: Dict[str, Dict[str, Any]] = {}
151
+
152
+ # # Async queue for orderly execution
153
+ # task_queue = asyncio.Queue()
154
+
155
+
156
+ # # -------------------------------------------------------------
157
+ # # ADD NEW TASK
158
+ # # -------------------------------------------------------------
159
+ # async def add_task(idea: str) -> str:
160
+ # """
161
+ # Adds a new ad generation task to the queue and returns its task ID.
162
+ # """
163
+ # task_id = str(uuid.uuid4())
164
+
165
+ # tasks[task_id] = {
166
+ # "id": task_id,
167
+ # "idea": idea,
168
+ # "status": TaskStatus.PENDING,
169
+ # "result": None,
170
+ # "confirmation_required": False
171
+ # }
172
+
173
+ # await task_queue.put(task_id)
174
+ # print(f"🧩 Task added to queue: {task_id}")
175
+
176
+ # return task_id
177
+
178
+
179
+ # # -------------------------------------------------------------
180
+ # # MAIN WORKER LOOP
181
+ # # -------------------------------------------------------------
182
+ # async def worker():
183
+ # """
184
+ # Continuously consumes tasks from the queue one-by-one.
185
+ # Each task runs through all stages in sequence.
186
+ # """
187
+ # while True:
188
+ # task_id = await task_queue.get()
189
+ # task = tasks.get(task_id)
190
+ # if not task:
191
+ # task_queue.task_done()
192
+ # continue
193
+
194
+ # try:
195
+ # print(f"πŸš€ Starting task: {task_id}")
196
+ # task["status"] = TaskStatus.RUNNING
197
+
198
+ # # === STEP 1: Script generation ===
199
+ # script_result = await generate_script(task["idea"])
200
+ # task["result"] = {"script": script_result}
201
+ # task["status"] = TaskStatus.WAITING_CONFIRMATION
202
+ # task["confirmation_required"] = True
203
+
204
+ # print(f"βœ‹ Task {task_id} waiting for confirmation after script stage.")
205
+
206
+ # # Wait until user confirms externally
207
+ # while task["status"] == TaskStatus.WAITING_CONFIRMATION:
208
+ # await asyncio.sleep(1)
209
+
210
+ # # === STEP 2: Story generation ===
211
+ # if task["status"] == TaskStatus.CONFIRMED:
212
+ # print(f"🎬 Task {task_id} confirmed. Continuing pipeline.pipeline...")
213
+
214
+ # story_result = await generate_story(script_result)
215
+ # # images = await generate_images(story_result)
216
+ # # video = await generate_video(images)
217
+ # # music = await generate_music(story_result)
218
+ # # final_output = await assemble_final_video(video, music)
219
+
220
+ # task["result"].update({
221
+ # "story_script": story_result,
222
+ # # "images": images,
223
+ # # "video": video,
224
+ # # "music": music,
225
+ # # "final_output": final_output
226
+ # })
227
+
228
+ # task["status"] = TaskStatus.COMPLETED
229
+ # print(f"βœ… Task {task_id} completed successfully!")
230
+
231
+ # except Exception as e:
232
+ # task["status"] = TaskStatus.FAILED
233
+ # task["result"] = {"error": str(e)}
234
+ # print(f"❌ Task {task_id} failed with error: {e}")
235
+
236
+ # finally:
237
+ # task_queue.task_done()
238
+
239
+
240
+ # # -------------------------------------------------------------
241
+ # # CONFIRMATION HANDLER
242
+ # # -------------------------------------------------------------
243
+ # async def confirm_task(task_id: str) -> Dict[str, Any]:
244
+ # """
245
+ # Confirms a paused task and resumes the rest of the pipeline.pipeline.
246
+ # """
247
+ # task = tasks.get(task_id)
248
+ # if not task:
249
+ # return {"error": "Invalid task ID."}
250
+
251
+ # if task["status"] != TaskStatus.WAITING_CONFIRMATION:
252
+ # return {"error": "Task is not waiting for confirmation."}
253
+
254
+ # task["status"] = TaskStatus.CONFIRMED
255
+ # task["confirmation_required"] = False
256
+ # print(f"πŸ‘ Task {task_id} confirmed. Resuming pipeline.pipeline...")
257
+
258
+ # return {"message": f"Task {task_id} confirmed and resumed."}
259
+
260
+
261
+ # # -------------------------------------------------------------
262
+ # # STATUS CHECK
263
+ # # -------------------------------------------------------------
264
+ # def get_task_status(task_id: str) -> Optional[Dict[str, Any]]:
265
+ # """
266
+ # Returns the full details of a task including stage results.
267
+ # """
268
+ # return tasks.get(task_id)
269
+
270
+
271
+ # # -------------------------------------------------------------
272
+ # # START WORKER ON APP STARTUP
273
+ # # -------------------------------------------------------------
274
+ # def start_worker():
275
+ # """
276
+ # Starts the asynchronous background worker loop.
277
+ # Must be called once when FastAPI app starts.
278
+ # """
279
+ # loop = asyncio.get_event_loop()
280
+ # loop.create_task(worker())
281
+ # print("βš™οΈ Worker loop started.")
282
+
283
+
284
+
285
+ import asyncio
286
+ import uuid
287
+ from typing import Dict, Any, Optional
288
+ from enum import Enum
289
+ from core.script_gen import generate_script
290
+ from core.story_script import generate_story
291
+ # from core.image_gen import generate_images
292
+ # from core.video_gen import generate_video
293
+ # from core.music_gen import generate_music
294
+ # from core.assemble import assemble_final_video
295
+
296
+ # -------------------------------------------------------------
297
+ # ENUMS AND GLOBALS
298
+ # -------------------------------------------------------------
299
+ class TaskStatus(str, Enum):
300
+ PENDING = "pending"
301
+ RUNNING = "running"
302
+ WAITING_CONFIRMATION = "waiting_for_confirmation"
303
+ CONFIRMED = "confirmed"
304
+ COMPLETED = "completed"
305
+ FAILED = "failed"
306
+
307
+ tasks: Dict[str, Dict[str, Any]] = {}
308
+ task_queue = asyncio.Queue()
309
+
310
+ # -------------------------------------------------------------
311
+ # ADD NEW TASK
312
+ # -------------------------------------------------------------
313
+ async def add_task(idea: str) -> str:
314
+ task_id = str(uuid.uuid4())
315
+ tasks[task_id] = {
316
+ "id": task_id,
317
+ "idea": idea,
318
+ "status": TaskStatus.PENDING,
319
+ "result": None,
320
+ "confirmation_required": False
321
+ }
322
+ await task_queue.put(task_id)
323
+ print(f"🧩 Task added to queue: {task_id}")
324
+ return task_id
325
+
326
+ # -------------------------------------------------------------
327
+ # WAIT FOR SCRIPT FOR CONFIRMATION
328
+ # -------------------------------------------------------------
329
+ async def wait_for_script(task_id: str, script_results: dict):
330
+ task = tasks.get(task_id)
331
+ if not task:
332
+ return
333
+
334
+ task["status"] = TaskStatus.RUNNING
335
+ # Generate script
336
+ script_result = await generate_script(task["idea"])
337
+ task["result"] = {"script": script_result}
338
+ task["status"] = TaskStatus.WAITING_CONFIRMATION
339
+ task["confirmation_required"] = True
340
+
341
+ # Keep script accessible for server endpoint
342
+ script_results[task_id] = script_result
343
+ print(f"βœ‹ Task {task_id} waiting for confirmation. Script ready.")
344
+
345
+ # -------------------------------------------------------------
346
+ # GENERATE STORY AFTER CONFIRMATION
347
+ # -------------------------------------------------------------
348
+ async def generate_story_after_confirm(script: str):
349
+ story_result = await generate_story(script)
350
+ return story_result
351
+
352
+ # -------------------------------------------------------------
353
+ # WORKER LOOP (Optional: future stages)
354
+ # -------------------------------------------------------------
355
+ async def worker():
356
+ while True:
357
+ task_id = await task_queue.get()
358
+ task = tasks.get(task_id)
359
+ if not task:
360
+ task_queue.task_done()
361
+ continue
362
+ try:
363
+ # Already handled script in wait_for_script
364
+ # Future stages like images/video/music can go here
365
+ pass
366
+ except Exception as e:
367
+ task["status"] = TaskStatus.FAILED
368
+ task["result"] = {"error": str(e)}
369
+ print(f"❌ Task {task_id} failed with error: {e}")
370
+ finally:
371
+ task_queue.task_done()
372
+
373
+ # -------------------------------------------------------------
374
+ # CONFIRM TASK
375
+ # -------------------------------------------------------------
376
+ async def confirm_task(task_id: str):
377
+ task = tasks.get(task_id)
378
+ if not task:
379
+ return {"error": "Invalid task ID."}
380
+
381
+ if task["status"] != TaskStatus.WAITING_CONFIRMATION:
382
+ return {"error": "Task is not waiting for confirmation."}
383
+
384
+ task["status"] = TaskStatus.CONFIRMED
385
+ task["confirmation_required"] = False
386
+ print(f"πŸ‘ Task {task_id} confirmed. Ready for story generation...")
387
+ return {"message": f"Task {task_id} confirmed."}
388
+
389
+ # -------------------------------------------------------------
390
+ # GET TASK STATUS
391
+ # -------------------------------------------------------------
392
+ def get_task_status(task_id: str) -> Optional[Dict[str, Any]]:
393
+ return tasks.get(task_id)
394
+
395
+ # -------------------------------------------------------------
396
+ # START WORKER
397
+ # -------------------------------------------------------------
398
+ def start_worker():
399
+ loop = asyncio.get_event_loop()
400
+ loop.create_task(worker())
401
+ print("βš™οΈ Worker loop started.")