Spaces:
Running
Running
Download apps/api/dist/queue/clip-scoring.processor.js from Benaah/MicProtocol: direct link, hf CLI and curl.
- Browser
- Download file 15.6 kB
-
https://huggingface.co/spaces/Benaah/MicProtocol/resolve/main/apps/api/dist/queue/clip-scoring.processor.js
- Command line
-
hf download hf://spaces/Benaah/MicProtocol/apps/api/dist/queue/clip-scoring.processor.js
-
curl -L -o clip-scoring.processor.js https://huggingface.co/spaces/Benaah/MicProtocol/resolve/main/apps/api/dist/queue/clip-scoring.processor.js
15.6 kB
| ; | |
| var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { | |
| if (k2 === undefined) k2 = k; | |
| var desc = Object.getOwnPropertyDescriptor(m, k); | |
| if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { | |
| desc = { enumerable: true, get: function() { return m[k]; } }; | |
| } | |
| Object.defineProperty(o, k2, desc); | |
| }) : (function(o, m, k, k2) { | |
| if (k2 === undefined) k2 = k; | |
| o[k2] = m[k]; | |
| })); | |
| var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { | |
| Object.defineProperty(o, "default", { enumerable: true, value: v }); | |
| }) : function(o, v) { | |
| o["default"] = v; | |
| }); | |
| var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { | |
| var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; | |
| if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); | |
| else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; | |
| return c > 3 && r && Object.defineProperty(target, key, r), r; | |
| }; | |
| var __importStar = (this && this.__importStar) || (function () { | |
| var ownKeys = function(o) { | |
| ownKeys = Object.getOwnPropertyNames || function (o) { | |
| var ar = []; | |
| for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; | |
| return ar; | |
| }; | |
| return ownKeys(o); | |
| }; | |
| return function (mod) { | |
| if (mod && mod.__esModule) return mod; | |
| var result = {}; | |
| if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); | |
| __setModuleDefault(result, mod); | |
| return result; | |
| }; | |
| })(); | |
| var __metadata = (this && this.__metadata) || function (k, v) { | |
| if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); | |
| }; | |
| var __param = (this && this.__param) || function (paramIndex, decorator) { | |
| return function (target, key) { decorator(target, key, paramIndex); } | |
| }; | |
| var ClipScoringProcessor_1; | |
| Object.defineProperty(exports, "__esModule", { value: true }); | |
| exports.ClipScoringProcessor = void 0; | |
| const bullmq_1 = require("@nestjs/bullmq"); | |
| const common_1 = require("@nestjs/common"); | |
| const child_process_1 = require("child_process"); | |
| const util_1 = require("util"); | |
| const path = __importStar(require("path")); | |
| const fs = __importStar(require("fs/promises")); | |
| const os = __importStar(require("os")); | |
| const prisma_service_1 = require("../prisma/prisma.service"); | |
| const r2_storage_service_1 = require("../common/storage/r2-storage.service"); | |
| const redis_constants_1 = require("../common/redis.constants"); | |
| const safe_fetch_1 = require("../common/safe-fetch"); | |
| const execAsync = (0, util_1.promisify)(child_process_1.execFile); | |
| const RELEASE_LOCK_SCRIPT = ` | |
| if redis.call("get", KEYS[1]) == ARGV[1] then | |
| return redis.call("del", KEYS[1]) | |
| else | |
| return 0 | |
| end | |
| `; | |
| const EXTEND_LOCK_SCRIPT = ` | |
| if redis.call("get", KEYS[1]) == ARGV[1] then | |
| return redis.call("pexpire", KEYS[1], ARGV[2]) | |
| else | |
| return 0 | |
| end | |
| `; | |
| let ClipScoringProcessor = ClipScoringProcessor_1 = class ClipScoringProcessor extends bullmq_1.WorkerHost { | |
| prisma; | |
| r2Storage; | |
| logger = new common_1.Logger(ClipScoringProcessor_1.name); | |
| redis; | |
| releaseLockSha = null; | |
| extendLockSha = null; | |
| constructor(prisma, r2Storage, redis) { | |
| super(); | |
| this.prisma = prisma; | |
| this.r2Storage = r2Storage; | |
| this.redis = redis; | |
| } | |
| async onModuleInit() { | |
| try { | |
| this.releaseLockSha = await this.redis.scriptLoad(RELEASE_LOCK_SCRIPT); | |
| this.extendLockSha = await this.redis.scriptLoad(EXTEND_LOCK_SCRIPT); | |
| this.logger.log("Lua scripts compiled for clip-scoring locks"); | |
| } | |
| catch (err) { | |
| this.logger.warn("Failed to pre-compile Lua scripts, will use EVAL fallback"); | |
| } | |
| } | |
| async process(job) { | |
| const { clipId, episodeId, videoUrl, transcriptText } = job.data; | |
| this.logger.log(`Processing clip scoring for ${clipId}`); | |
| const lockKey = `lock:clip:${clipId}`; | |
| const lockValue = `worker:${job.id}:${Date.now()}`; | |
| const lockTTL = 300000; | |
| const acquired = await this.redis.set(lockKey, lockValue, { | |
| NX: true, | |
| PX: lockTTL, | |
| }); | |
| if (!acquired) { | |
| await job.log(`Skipping job ${job.id}: clip ${clipId} is already being scored`); | |
| return { clipId, status: "skipped", reason: "locked" }; | |
| } | |
| const heartbeat = setInterval(async () => { | |
| try { | |
| const extended = await this.evalLua(this.extendLockSha, EXTEND_LOCK_SCRIPT, [lockKey], [lockValue, String(lockTTL)]); | |
| if (!extended) { | |
| this.logger.warn(`Lock lost for clip ${clipId}, aborting heartbeat`); | |
| clearInterval(heartbeat); | |
| } | |
| } | |
| catch (err) { | |
| this.logger.error(`Heartbeat extend failed: ${err}`); | |
| } | |
| }, 60000); | |
| const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), `clip-score-${clipId}-`)); | |
| try { | |
| await job.updateProgress(5); | |
| const sourcePath = path.join(tmpDir, "source.mp4"); | |
| await this.downloadVideo(videoUrl, sourcePath); | |
| await job.updateProgress(15); | |
| const clipBounds = await this.prisma.clip.findUnique({ | |
| where: { id: clipId }, | |
| select: { startSeconds: true, endSeconds: true }, | |
| }); | |
| const clipStart = clipBounds?.startSeconds ?? 0; | |
| const clipEnd = clipBounds?.endSeconds ?? 0; | |
| const clipDuration = Math.max(0, clipEnd - clipStart); | |
| const trimmedPath = path.join(tmpDir, "trimmed.mp4"); | |
| await this.trimSource(sourcePath, trimmedPath, clipStart, clipDuration); | |
| await job.updateProgress(25); | |
| const framesDir = path.join(tmpDir, "frames"); | |
| await fs.mkdir(framesDir, { recursive: true }); | |
| await this.extractFrames(trimmedPath, framesDir); | |
| await job.updateProgress(40); | |
| const audioPath = path.join(tmpDir, "audio.wav"); | |
| await this.extractAudio(trimmedPath, audioPath); | |
| await job.updateProgress(50); | |
| const pythonResult = await this.runPythonPipeline(framesDir, audioPath, transcriptText, tmpDir); | |
| await job.updateProgress(85); | |
| if (pythonResult.status === "error") { | |
| throw new Error(`Python scoring failed: ${pythonResult.message}`); | |
| } | |
| await this.prisma.clip.update({ | |
| where: { id: clipId }, | |
| data: { | |
| visualScore: Math.round(pythonResult.breakdown.visualAesthetic), | |
| audioScore: Math.round(pythonResult.breakdown.audioCompliance), | |
| transcriptScore: Math.round(pythonResult.breakdown.narrativeHook), | |
| scoringMetadata: pythonResult.metadata, | |
| scoredAt: new Date(), | |
| }, | |
| }); | |
| await job.updateProgress(100); | |
| this.logger.log(`Clip ${clipId} scored: overall=${pythonResult.overallScore}, ` + | |
| `visual=${pythonResult.breakdown.visualAesthetic}, ` + | |
| `audio=${pythonResult.breakdown.audioCompliance}, ` + | |
| `hook=${pythonResult.breakdown.narrativeHook}`); | |
| return { | |
| clipId, | |
| status: "completed", | |
| overallScore: pythonResult.overallScore, | |
| breakdown: pythonResult.breakdown, | |
| }; | |
| } | |
| catch (error) { | |
| const msg = error instanceof Error ? error.message : String(error); | |
| this.logger.error(`Clip scoring failed for ${clipId}: ${msg}`); | |
| throw error; | |
| } | |
| finally { | |
| clearInterval(heartbeat); | |
| await this.releaseLock(lockKey, lockValue); | |
| await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => undefined); | |
| } | |
| } | |
| async evalLua(sha, script, keys, args) { | |
| if (sha) { | |
| try { | |
| return await this.redis.evalSha(sha, { keys, arguments: args }); | |
| } | |
| catch (err) { | |
| if (err?.message?.includes("NOSCRIPT")) { | |
| return await this.redis.eval(script, { keys, arguments: args }); | |
| } | |
| throw err; | |
| } | |
| } | |
| return await this.redis.eval(script, { keys, arguments: args }); | |
| } | |
| async releaseLock(lockKey, lockValue) { | |
| try { | |
| await this.evalLua(this.releaseLockSha, RELEASE_LOCK_SCRIPT, [lockKey], [lockValue]); | |
| } | |
| catch (err) { | |
| this.logger.error(`Failed to release lock ${lockKey}: ${err}`); | |
| } | |
| } | |
| async downloadVideo(url, destPath) { | |
| let downloadUrl = url; | |
| if (!url.startsWith("http")) { | |
| downloadUrl = await this.r2Storage.getPresignedUrl(url, 3600); | |
| } | |
| const r2PublicHost = process.env.R2_PUBLIC_HOST || "r2.cloudflarestorage.com"; | |
| const allowedHostSuffixes = [ | |
| `.${r2PublicHost}`, | |
| ".s3.amazonaws.com", | |
| ".storage.googleapis.com", | |
| ".blob.core.windows.net", | |
| ".digitaloceanspaces.com", | |
| ]; | |
| try { | |
| (0, safe_fetch_1.assertSafeUrl)(downloadUrl, { allowedHostSuffixes }); | |
| } | |
| catch (err) { | |
| try { | |
| const u = new URL(downloadUrl); | |
| (0, safe_fetch_1.assertSafeUrl)(downloadUrl, { allowedHosts: [u.hostname] }); | |
| } | |
| catch { | |
| throw err; | |
| } | |
| } | |
| const response = await fetch(downloadUrl); | |
| if (!response.ok) { | |
| throw new Error(`Failed to download video: ${response.status} ${response.statusText}`); | |
| } | |
| const MAX_VIDEO_BYTES = 10 * 1024 * 1024 * 1024; | |
| const contentLength = Number(response.headers.get("content-length")); | |
| if (Number.isFinite(contentLength) && contentLength > MAX_VIDEO_BYTES) { | |
| throw new Error(`Video too large (${contentLength} bytes exceeds the ${MAX_VIDEO_BYTES}-byte cap)`); | |
| } | |
| if (!response.body) { | |
| throw new Error("Downloaded video response has no body stream"); | |
| } | |
| let totalBytes = 0; | |
| const reader = response.body.getReader(); | |
| try { | |
| const fileHandle = await fs.open(destPath, "w"); | |
| try { | |
| for (;;) { | |
| const { done, value } = await reader.read(); | |
| if (done) | |
| break; | |
| totalBytes += value.byteLength; | |
| if (totalBytes > MAX_VIDEO_BYTES) { | |
| throw new Error(`Video exceeds the ${MAX_VIDEO_BYTES}-byte cap; aborting download`); | |
| } | |
| await fileHandle.write(value); | |
| } | |
| } | |
| finally { | |
| await fileHandle.close().catch(() => undefined); | |
| } | |
| } | |
| finally { | |
| reader.cancel().catch(() => undefined); | |
| } | |
| } | |
| async trimSource(sourcePath, outputPath, startSeconds, durationSeconds) { | |
| const args = [ | |
| "-y", | |
| "-ss", String(Math.max(0, startSeconds)), | |
| "-i", sourcePath, | |
| "-t", String(Math.max(0, durationSeconds)), | |
| "-c", "copy", | |
| "-avoid_negative_ts", "make_zero", | |
| outputPath, | |
| ]; | |
| try { | |
| await execAsync("ffmpeg", args, { timeout: 60000 }); | |
| } | |
| catch (err) { | |
| const fallbackArgs = [ | |
| "-y", | |
| "-ss", String(Math.max(0, startSeconds)), | |
| "-i", sourcePath, | |
| "-t", String(Math.max(0, durationSeconds)), | |
| "-c:v", "libx264", | |
| "-c:a", "aac", | |
| "-preset", "ultrafast", | |
| outputPath, | |
| ]; | |
| await execAsync("ffmpeg", fallbackArgs, { timeout: 180000 }); | |
| } | |
| } | |
| async extractFrames(sourcePath, framesDir) { | |
| const outPattern = path.join(framesDir, "frame_%04d.jpg"); | |
| const args = [ | |
| "-y", | |
| "-i", sourcePath, | |
| "-vf", "fps=1,scale=224:224:force_original_aspect_ratio=decrease,pad=224:224:(ow-iw)/2:(oh-ih)/2", | |
| "-q:v", "2", | |
| outPattern, | |
| ]; | |
| try { | |
| await execAsync("ffmpeg", args, { timeout: 120000 }); | |
| } | |
| catch (err) { | |
| const message = err?.message || String(err); | |
| if (message.includes("No such file or directory")) { | |
| throw new Error("FFmpeg not found. Ensure FFmpeg is installed in the worker container."); | |
| } | |
| throw new Error(`FFmpeg frame extraction failed: ${message}`); | |
| } | |
| } | |
| async extractAudio(sourcePath, audioPath) { | |
| const args = [ | |
| "-y", | |
| "-i", sourcePath, | |
| "-vn", | |
| "-acodec", "pcm_s16le", | |
| "-ar", "44100", | |
| audioPath, | |
| ]; | |
| try { | |
| await execAsync("ffmpeg", args, { timeout: 60000 }); | |
| } | |
| catch (err) { | |
| throw new Error(`Audio extraction failed: ${err?.message || String(err)}`); | |
| } | |
| } | |
| async runPythonPipeline(framesDir, audioPath, transcript, tmpDir) { | |
| const possiblePaths = [ | |
| path.resolve(process.cwd(), "services/clip-scorer/pipeline.py"), | |
| path.resolve("/app/scripts/pipeline.py"), | |
| path.resolve(__dirname, "../../scripts/pipeline.py"), | |
| ]; | |
| let scriptPath = ""; | |
| for (const p of possiblePaths) { | |
| try { | |
| await fs.access(p); | |
| scriptPath = p; | |
| break; | |
| } | |
| catch { | |
| continue; | |
| } | |
| } | |
| if (!scriptPath) { | |
| throw new Error("Python pipeline script not found. Searched: " + possiblePaths.join(", ")); | |
| } | |
| const transcriptPath = path.join(tmpDir, "transcript.txt"); | |
| await fs.writeFile(transcriptPath, transcript, "utf8"); | |
| const args = [ | |
| scriptPath, | |
| "--frames_dir", framesDir, | |
| "--audio_path", audioPath, | |
| "--transcript_path", transcriptPath, | |
| ]; | |
| const { stdout, stderr } = await execAsync("python3", args, { | |
| timeout: 300000, | |
| maxBuffer: 1024 * 1024, | |
| }); | |
| if (!stdout.trim()) { | |
| throw new Error(`Python script produced no output. stderr: ${stderr}`); | |
| } | |
| return JSON.parse(stdout.trim()); | |
| } | |
| }; | |
| exports.ClipScoringProcessor = ClipScoringProcessor; | |
| exports.ClipScoringProcessor = ClipScoringProcessor = ClipScoringProcessor_1 = __decorate([ | |
| (0, bullmq_1.Processor)("clip-scoring", { | |
| concurrency: 2, | |
| }), | |
| __param(2, (0, common_1.Inject)(redis_constants_1.REDIS_CLIENT)), | |
| __metadata("design:paramtypes", [prisma_service_1.PrismaService, | |
| r2_storage_service_1.R2StorageService, Object]) | |
| ], ClipScoringProcessor); | |
| //# sourceMappingURL=clip-scoring.processor.js.map |