add funscripts tests
@ -55,4 +55,8 @@ In other words, pick something for a name and roll with the punches.
|
||||
### Find what you love and let it kill you
|
||||
|
||||
> Find what you love and let it kill you. Let it drain you of your all. Let it cling onto your back and weigh you down into eventual nothingness. Let it kill you and let it devour your remains. For all things will kill you, both slowly and fastly, but it's much better to be killed by a lover.
|
||||
> -- Charles Bukowski
|
||||
> -- Charles Bukowski
|
||||
|
||||
### Do what you can, with what you have, where you are at
|
||||
|
||||
### Use it up, wear it out, make it do, or do without
|
@ -1,3 +1,5 @@
|
||||
src/test
|
||||
|
||||
node_modules
|
||||
|
||||
# Output
|
||||
|
2
services/our/.gitignore
vendored
@ -1,3 +1,5 @@
|
||||
vibeui
|
||||
|
||||
generated
|
||||
|
||||
backup
|
||||
|
@ -13,8 +13,11 @@ RUN apt-get update -y && \
|
||||
mktorrent \
|
||||
python3 \
|
||||
python3-pip \
|
||||
python3-venv \
|
||||
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install IPFS Kubo
|
||||
COPY --from=ipfs/kubo:v0.36.0 /usr/local/bin/ipfs /usr/local/bin/ipfs
|
||||
|
||||
# Copy and install dependencies
|
||||
COPY package.json package-lock.json ./
|
||||
|
@ -5,7 +5,7 @@ services:
|
||||
container_name: our-postgres
|
||||
image: postgres:17
|
||||
restart: unless-stopped
|
||||
env_file: ./../../.env
|
||||
env_file: ./../../.env.development
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
@ -17,47 +17,6 @@ services:
|
||||
start_period: 10s
|
||||
timeout: 10s
|
||||
|
||||
pgweb:
|
||||
container_name: out-pgweb
|
||||
image: sosedoff/pgweb
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
env_file: ./../../.env
|
||||
ports:
|
||||
- "8091:8081"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8081"]
|
||||
interval: 10s
|
||||
retries: 5
|
||||
start_period: 10s
|
||||
timeout: 10s
|
||||
|
||||
# pgadmin:
|
||||
# image: dpage/pgadmin4:latest
|
||||
# environment:
|
||||
# PGADMIN_LISTEN_PORT: 5050
|
||||
# PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL}
|
||||
# PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD}
|
||||
# PGADMIN_DISABLE_POSTFIX: true
|
||||
# ports:
|
||||
# - "5050:5050"
|
||||
|
||||
our:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: our-app
|
||||
ports:
|
||||
- "5000:5000"
|
||||
env_file: ./../../.env ## @see ./src/config/env.ts for all env var names.
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- default
|
||||
entrypoint: >
|
||||
sh -c "npm run deploy && npm run start:server"
|
||||
|
||||
volumes:
|
||||
pgdata:
|
@ -66,4 +66,3 @@ networks:
|
||||
default: {}
|
||||
|
||||
|
||||
## i need to mount /mnt/vfs/futureporn:/mnt/vfs/futureporn on both the server and worker
|
53
services/our/compose.staging.yaml
Normal file
@ -0,0 +1,53 @@
|
||||
|
||||
services:
|
||||
|
||||
postgres:
|
||||
container_name: our-postgres
|
||||
image: postgres:17
|
||||
restart: unless-stopped
|
||||
env_file: ./../../.env.development
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD", "pg_isready", "-U", "postgres"]
|
||||
interval: 10s
|
||||
retries: 5
|
||||
start_period: 10s
|
||||
timeout: 10s
|
||||
|
||||
server:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: our-server
|
||||
ports:
|
||||
- "5000:5000"
|
||||
env_file: ./../../.env.development ## @see ./src/config/env.ts for all env var names.
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- default
|
||||
entrypoint: >
|
||||
sh -c "npm run deploy && npm run start:server"
|
||||
|
||||
worker:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: our-worker
|
||||
env_file: ./../../.env.development
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- default
|
||||
init: true
|
||||
entrypoint: >
|
||||
sh -c "npm run start:worker"
|
||||
|
||||
|
||||
volumes:
|
||||
pgdata:
|
@ -3,9 +3,6 @@
|
||||
"private": true,
|
||||
"version": "2.0.1",
|
||||
"type": "module",
|
||||
"overrides": {
|
||||
"@sinclair/typebox": "0.34.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "concurrently npm:dev:serve npm:dev:build npm:dev:worker",
|
||||
"dev:serve": "tsx watch ./src/index.ts",
|
||||
@ -18,26 +15,21 @@
|
||||
"build": "tsup --clean",
|
||||
"lint": "eslint .",
|
||||
"clean": "rm -rf node_modules && rm -rf pnpm-lock.yaml",
|
||||
"deploy": "npx prisma migrate deploy"
|
||||
"deploy": "npx prisma migrate deploy",
|
||||
"test:watch": "npx vitest --watch",
|
||||
"test": "npx vitest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "^1.3.1",
|
||||
"@eslint/js": "^9.31.0",
|
||||
"@fontsource/fira-mono": "^5.2.6",
|
||||
"@neoconfetti/svelte": "^2.2.2",
|
||||
"@sveltejs/adapter-auto": "^6.0.1",
|
||||
"@sveltejs/kit": "^2.22.5",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.1.1",
|
||||
"eslint": "^9.31.0",
|
||||
"eslint-plugin-svelte": "^3.10.1",
|
||||
"globals": "^16.3.0",
|
||||
"nodemon": "^3.1.10",
|
||||
"prisma": "6.8.2",
|
||||
"svelte": "^5.35.6",
|
||||
"svelte-check": "^4.2.2",
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint": "^8.36.0",
|
||||
"vite": "^6.3.5"
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
@ -45,13 +37,9 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@auth0/auth0-spa-js": "^2.2.0",
|
||||
"@aws-sdk/client-s3": "3.726.1",
|
||||
"@aws-sdk/s3-request-presigner": "^3.844.0",
|
||||
"@bogeychan/elysia-logger": "^0.1.8",
|
||||
"@dotenvx/dotenvx": "^1.47.5",
|
||||
"@elysiajs/static": "^1.3.0",
|
||||
"@elysiajs/swagger": "^1.3.1",
|
||||
"@fastify/flash": "^6.0.3",
|
||||
"@fastify/formbody": "^8.0.2",
|
||||
"@fastify/multipart": "^9.0.3",
|
||||
@ -69,19 +57,10 @@
|
||||
"@prisma/extension-accelerate": "^1.3.0",
|
||||
"@types/node": "^22.16.3",
|
||||
"@types/node-fetch": "^2.6.12",
|
||||
"arctic": "^3.7.0",
|
||||
"axios": "^1.10.0",
|
||||
"cache-manager": "^7.0.1",
|
||||
"chokidar-cli": "^3.0.0",
|
||||
"concurrently": "^9.2.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"drizzle-orm": "^0.43.1",
|
||||
"drizzle-typebox": "^0.3.3",
|
||||
"ejs": "^3.1.10",
|
||||
"elysia": "^1.3.5",
|
||||
"elysia-clerk": "^0.9.10",
|
||||
"elysia-connect-middleware": "^0.0.5",
|
||||
"elysia-oauth2": "^2.1.0",
|
||||
"fastify": "^5.4.0",
|
||||
"fastify-plugin": "^5.0.1",
|
||||
"fastify-sse-v2": "^4.2.1",
|
||||
@ -96,8 +75,8 @@
|
||||
"mime-types": "^3.0.1",
|
||||
"nano-spawn": "^1.0.2",
|
||||
"nanoid": "^5.1.5",
|
||||
"nocodb-sdk": "^0.263.8",
|
||||
"node-fetch": "^3.3.2",
|
||||
"onnxruntime-node": "1.22.0-rev",
|
||||
"rate-limiter-flexible": "^7.1.1",
|
||||
"rimraf": "6.0.1",
|
||||
"sharp": "^0.34.3",
|
||||
@ -105,7 +84,7 @@
|
||||
"ts-node": "^10.9.2",
|
||||
"tsup": "^8.5.0",
|
||||
"tsx": "^4.20.3",
|
||||
"vitest": "^3.2.4",
|
||||
"which": "^5.0.0",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"prisma": {
|
||||
|
7413
services/our/pnpm-lock.yaml
generated
Normal file
@ -1,6 +1,10 @@
|
||||
// ./config/env.ts
|
||||
import { z } from 'zod';
|
||||
import '@dotenvx/dotenvx/config'
|
||||
import dotenvx from '@dotenvx/dotenvx'
|
||||
|
||||
dotenvx.config({ path: ['../../.env.development'] })
|
||||
// if (process.env.NODE_ENV === 'development') {
|
||||
// }
|
||||
|
||||
const EnvSchema = z.object({
|
||||
NODE_ENV: z.enum(['development', 'production', 'test']),
|
||||
@ -26,7 +30,7 @@ const EnvSchema = z.object({
|
||||
});
|
||||
|
||||
const parsed = EnvSchema.safeParse(process.env);
|
||||
console.log(parsed)
|
||||
// console.log(parsed)
|
||||
|
||||
if (!parsed.success) {
|
||||
console.error('❌ Invalid environment variables:', parsed.error.flatten().fieldErrors);
|
||||
|
@ -1,370 +1,28 @@
|
||||
|
||||
import type { Task, Helpers } from "graphile-worker";
|
||||
import { PrismaClient } from "../../generated/prisma";
|
||||
import { withAccelerate } from "@prisma/extension-accelerate";
|
||||
import { getOrDownloadAsset } from "../utils/cache";
|
||||
import { env } from "../config/env";
|
||||
import { getS3Client, uploadFile } from "../utils/s3";
|
||||
import { nanoid } from "nanoid";
|
||||
import { existsSync } from "node:fs";
|
||||
import spawn from 'nano-spawn';
|
||||
import { join, basename, extname } from "node:path";
|
||||
import { readFile, writeFile, readdir } from 'node:fs/promises';
|
||||
import yaml from 'js-yaml';
|
||||
import { string } from "zod";
|
||||
|
||||
const prisma = new PrismaClient().$extends(withAccelerate());
|
||||
import { buildFunscript } from '../utils/funscripts';
|
||||
import { vibeuiInference } from "../utils/vibeui";
|
||||
import { join } from "node:path";
|
||||
|
||||
interface Payload {
|
||||
vodId: string;
|
||||
}
|
||||
|
||||
interface Detection {
|
||||
startFrame: number;
|
||||
endFrame: number;
|
||||
className: string;
|
||||
}
|
||||
|
||||
interface DataYaml {
|
||||
path: string;
|
||||
train: string;
|
||||
val: string;
|
||||
names: Record<string, string>;
|
||||
}
|
||||
|
||||
interface FunscriptAction {
|
||||
at: number;
|
||||
pos: number;
|
||||
}
|
||||
|
||||
interface Funscript {
|
||||
version: string;
|
||||
actions: FunscriptAction[];
|
||||
}
|
||||
|
||||
|
||||
interface ClassPositionMap {
|
||||
[className: string]: number | 'pattern';
|
||||
}
|
||||
const prisma = new PrismaClient().$extends(withAccelerate());
|
||||
|
||||
|
||||
function assertPayload(payload: any): asserts payload is Payload {
|
||||
if (typeof payload !== "object" || !payload) throw new Error("invalid payload-- was not an object.");
|
||||
if (typeof payload.vodId !== "string") throw new Error("invalid payload-- was missing vodId");
|
||||
}
|
||||
|
||||
async function loadDataYaml(yamlPath: string): Promise<DataYaml> {
|
||||
const yamlContent = await readFile(yamlPath, 'utf8');
|
||||
return yaml.load(yamlContent) as DataYaml;
|
||||
}
|
||||
|
||||
|
||||
async function preparePython(helpers) {
|
||||
const venvPath = join(env.VIBEUI_DIR, 'venv')
|
||||
|
||||
// Create venv if it doesn't exist
|
||||
if (!existsSync(venvPath)) {
|
||||
helpers.logger.info("Python venv not found. Creating one...");
|
||||
await spawn("python", ["-m", "venv", "venv"], {
|
||||
cwd: env.VIBEUI_DIR,
|
||||
});
|
||||
} else {
|
||||
helpers.logger.info("Using existing Python venv.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function ffprobe(videoPath: string): Promise<{ fps: number; frames: number }> {
|
||||
const { stdout } = await spawn('ffprobe', [
|
||||
'-v', 'error',
|
||||
'-select_streams', 'v:0',
|
||||
'-count_frames',
|
||||
'-show_entries', 'stream=nb_read_frames,r_frame_rate',
|
||||
'-of', 'default=nokey=1:noprint_wrappers=1',
|
||||
videoPath,
|
||||
])
|
||||
|
||||
const [frameRateStr, frameCountStr] = stdout.trim().split('\n')
|
||||
const [num, denom] = frameRateStr.trim().split('/').map(Number)
|
||||
const fps = num / denom
|
||||
const frames = parseInt(frameCountStr.trim(), 10)
|
||||
|
||||
return { fps, frames }
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export async function buildFunscript(
|
||||
helpers: Helpers,
|
||||
predictionOutput: string,
|
||||
videoPath: string
|
||||
): Promise<string> {
|
||||
const labelDir = join(predictionOutput, 'labels');
|
||||
const yamlPath = join(predictionOutput, 'data.yaml');
|
||||
const outputPath = join(process.env.CACHE_ROOT ?? '/tmp', `${nanoid()}.funscript`);
|
||||
helpers.logger.info('Starting Funscript generation');
|
||||
|
||||
try {
|
||||
|
||||
const data = await loadDataYaml(join(env.VIBEUI_DIR, 'data.yaml'))
|
||||
const classPositionMap = await loadClassPositionMap(data, helpers);
|
||||
const { fps, totalFrames } = await loadVideoMetadata(videoPath, helpers);
|
||||
const detectionSegments = await processLabelFiles(labelDir, helpers, data);
|
||||
const totalDurationMs = Math.floor((totalFrames / fps) * 1000);
|
||||
const actions = generateActions(totalDurationMs, fps, detectionSegments, classPositionMap);
|
||||
await writeFunscript(outputPath, actions, helpers);
|
||||
|
||||
return outputPath;
|
||||
} catch (error) {
|
||||
helpers.logger.error(`Error generating Funscript: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function inference(helpers: Helpers, videoFilePath: string): Promise<string> {
|
||||
const modelPath = join(env.VIBEUI_DIR, 'runs/detect/vibeui/weights/best.pt')
|
||||
|
||||
// Generate a unique name based on video name + UUID
|
||||
const videoExt = extname(videoFilePath) // e.g. '.mp4'
|
||||
const videoName = basename(videoFilePath, videoExt) // removes the extension
|
||||
const uniqueName = `${videoName}-${nanoid()}`
|
||||
const customProjectDir = 'runs' // or any custom folder
|
||||
const outputPath = join(env.VIBEUI_DIR, customProjectDir, uniqueName)
|
||||
|
||||
await spawn('./venv/bin/yolo', [
|
||||
'predict',
|
||||
`model=${modelPath}`,
|
||||
`source=${videoFilePath}`,
|
||||
'save=False',
|
||||
'save_txt=True',
|
||||
'save_conf=True',
|
||||
`project=${customProjectDir}`,
|
||||
`name=${uniqueName}`,
|
||||
], {
|
||||
cwd: env.VIBEUI_DIR,
|
||||
stdio: 'inherit',
|
||||
})
|
||||
|
||||
return outputPath // contains labels/ folder and predictions
|
||||
}
|
||||
|
||||
|
||||
async function loadClassPositionMap(data: DataYaml, helpers: Helpers): Promise<ClassPositionMap> {
|
||||
try {
|
||||
|
||||
if (
|
||||
!data ||
|
||||
typeof data !== 'object' ||
|
||||
!('names' in data) ||
|
||||
typeof data.names !== 'object' ||
|
||||
data.names === null ||
|
||||
Object.keys(data.names).length === 0
|
||||
) {
|
||||
throw new Error('Invalid data.yaml: "names" field is missing, not an object, or empty');
|
||||
}
|
||||
|
||||
const positionMap: ClassPositionMap = {
|
||||
ControlledByTipper: 50,
|
||||
ControlledByTipperHigh: 80,
|
||||
ControlledByTipperLow: 20,
|
||||
ControlledByTipperMedium: 50,
|
||||
ControlledByTipperUltrahigh: 95,
|
||||
Ring1: 30,
|
||||
Ring2: 40,
|
||||
Ring3: 50,
|
||||
Ring4: 60,
|
||||
Earthquake: 'pattern',
|
||||
Fireworks: 'pattern',
|
||||
Pulse: 'pattern',
|
||||
Wave: 'pattern',
|
||||
Pause: 0,
|
||||
RandomTime: 70,
|
||||
HighLevel: 80,
|
||||
LowLevel: 20,
|
||||
MediumLevel: 50,
|
||||
UltraHighLevel: 95
|
||||
};
|
||||
|
||||
const names = Object.values(data.names);
|
||||
for (const name of names) {
|
||||
if (typeof name !== 'string' || name.trim() === '') {
|
||||
helpers.logger.info(`Skipping invalid class name: ${name}`);
|
||||
continue;
|
||||
}
|
||||
if (!(name in positionMap)) {
|
||||
helpers.logger.info(`No position mapping for class "${name}", defaulting to 0`);
|
||||
positionMap[name] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
helpers.logger.info(`Loaded class position map: ${JSON.stringify(positionMap)}`);
|
||||
return positionMap;
|
||||
} catch (error) {
|
||||
helpers.logger.error(`Error loading data.yaml: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function generatePatternPositions(startMs: number, durationMs: number, className: string, fps: number): FunscriptAction[] {
|
||||
const actions: FunscriptAction[] = [];
|
||||
const frameDurationMs = 1000 / fps;
|
||||
const totalFrames = Math.floor(durationMs / frameDurationMs);
|
||||
const intervalMs = 100;
|
||||
|
||||
for (let timeMs = 0; timeMs < durationMs; timeMs += intervalMs) {
|
||||
const progress = timeMs / durationMs;
|
||||
let pos = 0;
|
||||
|
||||
switch (className) {
|
||||
case 'Pulse':
|
||||
pos = Math.round(50 * Math.sin(progress * 2 * Math.PI));
|
||||
break;
|
||||
case 'Wave':
|
||||
pos = Math.round(50 + 50 * Math.sin(progress * 2 * Math.PI));
|
||||
break;
|
||||
case 'Fireworks':
|
||||
pos = Math.random() > 0.5 ? 80 : 0;
|
||||
break;
|
||||
case 'Earthquake':
|
||||
pos = Math.round(90 * Math.sin(progress * 4 * Math.PI) + (Math.random() - 0.5) * 10);
|
||||
pos = Math.max(0, Math.min(90, pos));
|
||||
break;
|
||||
}
|
||||
|
||||
actions.push({ at: startMs + timeMs, pos });
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
async function loadVideoMetadata(videoPath: string, helpers: Helpers) {
|
||||
const { fps, frames: totalFrames } = await ffprobe(videoPath);
|
||||
helpers.logger.info(`Video metadata: fps=${fps}, frames=${totalFrames}`);
|
||||
return { fps, totalFrames };
|
||||
}
|
||||
|
||||
async function processLabelFiles(labelDir: string, helpers: Helpers, data: DataYaml): Promise<Detection[]> {
|
||||
const labelFiles = (await readdir(labelDir)).filter(file => file.endsWith('.txt'));
|
||||
const detections: Map<number, Detection[]> = new Map();
|
||||
const names = data.names;
|
||||
|
||||
for (const file of labelFiles) {
|
||||
const match = file.match(/(\d+)\.txt$/);
|
||||
if (!match) {
|
||||
helpers.logger.info(`Skipping invalid filename: ${file}`);
|
||||
continue;
|
||||
}
|
||||
const frameIndex = parseInt(match[1], 10);
|
||||
if (isNaN(frameIndex)) {
|
||||
helpers.logger.info(`Skipping invalid frame index from filename: ${file}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const content = await readFile(join(labelDir, file), 'utf8');
|
||||
const lines = content.trim().split('\n');
|
||||
const frameDetections: Detection[] = [];
|
||||
let maxConfidence = 0;
|
||||
let selectedClassIndex = -1;
|
||||
|
||||
for (const line of lines) {
|
||||
const parts = line.trim().split(/\s+/);
|
||||
if (parts.length < 6) continue;
|
||||
|
||||
const classIndex = parseInt(parts[0], 10);
|
||||
const confidence = parseFloat(parts[5]);
|
||||
if (isNaN(classIndex) || isNaN(confidence)) continue;
|
||||
|
||||
if (confidence >= 0.7 && confidence > maxConfidence) {
|
||||
maxConfidence = confidence;
|
||||
selectedClassIndex = classIndex;
|
||||
}
|
||||
}
|
||||
|
||||
if (maxConfidence > 0) {
|
||||
const className = (data.names as Record<string, string>)[selectedClassIndex.toString()];
|
||||
if (className) {
|
||||
frameDetections.push({ startFrame: frameIndex, endFrame: frameIndex, className });
|
||||
}
|
||||
}
|
||||
|
||||
if (frameDetections.length > 0) {
|
||||
detections.set(frameIndex, frameDetections);
|
||||
}
|
||||
}
|
||||
|
||||
// Merge overlapping detections into continuous segments
|
||||
const detectionSegments: Detection[] = [];
|
||||
let currentDetection: Detection | null = null;
|
||||
|
||||
for (const [frameIndex, frameDetections] of detections.entries()) {
|
||||
for (const detection of frameDetections) {
|
||||
if (!currentDetection || currentDetection.className !== detection.className) {
|
||||
if (currentDetection) detectionSegments.push(currentDetection);
|
||||
currentDetection = { ...detection, endFrame: frameIndex };
|
||||
} else {
|
||||
currentDetection.endFrame = frameIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (currentDetection) detectionSegments.push(currentDetection);
|
||||
|
||||
return detectionSegments;
|
||||
}
|
||||
|
||||
function generateActions(totalDurationMs: number, fps: number, detectionSegments: Detection[], classPositionMap: ClassPositionMap): FunscriptAction[] {
|
||||
const intervalMs = 100;
|
||||
const actions: FunscriptAction[] = [];
|
||||
|
||||
// Generate static position actions
|
||||
for (let timeMs = 0; timeMs <= totalDurationMs; timeMs += intervalMs) {
|
||||
const frameIndex = Math.floor((timeMs / 1000) * fps);
|
||||
let position = 0;
|
||||
|
||||
for (const segment of detectionSegments) {
|
||||
if (frameIndex >= segment.startFrame && frameIndex <= segment.endFrame) {
|
||||
const className = segment.className;
|
||||
if (typeof classPositionMap[className] === 'number') {
|
||||
position = classPositionMap[className];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
actions.push({ at: timeMs, pos: position });
|
||||
}
|
||||
|
||||
// Overlay pattern-based actions
|
||||
for (const segment of detectionSegments) {
|
||||
const className = segment.className;
|
||||
if (classPositionMap[className] === 'pattern') {
|
||||
const startMs = Math.floor((segment.startFrame / fps) * 1000);
|
||||
const durationMs = Math.floor(((segment.endFrame - segment.startFrame + 1) / fps) * 1000);
|
||||
const patternActions = generatePatternPositions(startMs, durationMs, className, fps);
|
||||
actions.push(...patternActions);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort actions by time and remove duplicates
|
||||
actions.sort((a, b) => a.at - b.at);
|
||||
const uniqueActions: FunscriptAction[] = [];
|
||||
let lastTime = -1;
|
||||
for (const action of actions) {
|
||||
if (action.at !== lastTime) {
|
||||
uniqueActions.push(action);
|
||||
lastTime = action.at;
|
||||
}
|
||||
}
|
||||
|
||||
return uniqueActions;
|
||||
}
|
||||
|
||||
async function writeFunscript(outputPath: string, actions: FunscriptAction[], helpers: Helpers) {
|
||||
const funscript: Funscript = { version: '1.0', actions };
|
||||
await writeFile(outputPath, JSON.stringify(funscript, null, 2));
|
||||
helpers.logger.info(`Funscript generated: ${outputPath} (${actions.length} actions)`);
|
||||
}
|
||||
|
||||
|
||||
const createFunscript: Task = async (payload: any, helpers: Helpers) => {
|
||||
assertPayload(payload);
|
||||
@ -373,7 +31,6 @@ const createFunscript: Task = async (payload: any, helpers: Helpers) => {
|
||||
|
||||
|
||||
const vod = await prisma.vod.findFirstOrThrow({ where: { id: vodId } });
|
||||
await preparePython(helpers)
|
||||
|
||||
if (vod.funscript) {
|
||||
helpers.logger.info(`Doing nothing-- vod ${vodId} already has a funscript.`);
|
||||
@ -394,11 +51,12 @@ const createFunscript: Task = async (payload: any, helpers: Helpers) => {
|
||||
|
||||
helpers.logger.info(`Creating funscript for vod ${vodId}...`);
|
||||
|
||||
const predictionOutput = await inference(helpers, videoFilePath);
|
||||
const modelPath = join(env.VIBEUI_DIR, 'vibeui.onnx')
|
||||
const predictionOutput = await vibeuiInference(modelPath, videoFilePath);
|
||||
helpers.logger.info(`prediction output ${predictionOutput}`);
|
||||
|
||||
|
||||
const funscriptFilePath = await buildFunscript(helpers, predictionOutput, videoFilePath)
|
||||
const funscriptFilePath = await buildFunscript(predictionOutput, videoFilePath)
|
||||
|
||||
|
||||
const s3Key = `funscripts/${vodId}.funscript`;
|
||||
|
452
services/our/src/tasks/createFunscript.ts.old
Normal file
@ -0,0 +1,452 @@
|
||||
import type { Task, Helpers } from "graphile-worker";
|
||||
import { PrismaClient } from "../../generated/prisma";
|
||||
import { withAccelerate } from "@prisma/extension-accelerate";
|
||||
import { getOrDownloadAsset } from "../utils/cache";
|
||||
import { env } from "../config/env";
|
||||
import { getS3Client, uploadFile } from "../utils/s3";
|
||||
import { nanoid } from "nanoid";
|
||||
import { existsSync, rmSync } from "node:fs";
|
||||
import { join, basename, extname } from "node:path";
|
||||
import { readFile, writeFile, readdir } from 'node:fs/promises';
|
||||
import yaml from 'js-yaml';
|
||||
import { getNanoSpawn } from "../utils/nanoSpawn";
|
||||
import which from "which";
|
||||
import * as ort from 'onnxruntime-node';
|
||||
|
||||
interface Payload {
|
||||
vodId: string;
|
||||
}
|
||||
|
||||
interface Detection {
|
||||
startFrame: number;
|
||||
endFrame: number;
|
||||
className: string;
|
||||
}
|
||||
|
||||
interface DataYaml {
|
||||
path: string;
|
||||
train: string;
|
||||
val: string;
|
||||
names: Record<string, string>;
|
||||
}
|
||||
|
||||
interface FunscriptAction {
|
||||
at: number;
|
||||
pos: number;
|
||||
}
|
||||
|
||||
interface Funscript {
|
||||
version: string;
|
||||
actions: FunscriptAction[];
|
||||
}
|
||||
|
||||
|
||||
interface ClassPositionMap {
|
||||
[className: string]: number | 'pattern';
|
||||
}
|
||||
|
||||
|
||||
const prisma = new PrismaClient().$extends(withAccelerate());
|
||||
|
||||
|
||||
function assertPayload(payload: any): asserts payload is Payload {
|
||||
if (typeof payload !== "object" || !payload) throw new Error("invalid payload-- was not an object.");
|
||||
if (typeof payload.vodId !== "string") throw new Error("invalid payload-- was missing vodId");
|
||||
}
|
||||
|
||||
async function loadDataYaml(yamlPath: string): Promise<DataYaml> {
|
||||
const yamlContent = await readFile(yamlPath, 'utf8');
|
||||
return yaml.load(yamlContent) as DataYaml;
|
||||
}
|
||||
|
||||
|
||||
// async function preparePython(helpers) {
|
||||
// const spawn = await getNanoSpawn();
|
||||
// const venvPath = join(env.VIBEUI_DIR, "venv");
|
||||
|
||||
// // Determine Python executable
|
||||
// let pythonCmd;
|
||||
// try {
|
||||
// pythonCmd = which.sync("python3");
|
||||
// } catch {
|
||||
// helpers.logger.error("Python is not installed or not in PATH.");
|
||||
// throw new Error("Python not found in PATH.");
|
||||
// }
|
||||
|
||||
// // If venv doesn't exist, create it
|
||||
// if (!existsSync(venvPath)) {
|
||||
// helpers.logger.info("Python venv not found. Creating one...");
|
||||
|
||||
// try {
|
||||
// await spawn(pythonCmd, ["-m", "venv", "venv"], {
|
||||
// cwd: env.VIBEUI_DIR,
|
||||
// });
|
||||
|
||||
// helpers.logger.info("Python venv successfully created.");
|
||||
// } catch (err) {
|
||||
// helpers.logger.error("Failed to create Python venv:", err);
|
||||
|
||||
// // Clean up partially created venv if needed
|
||||
// try {
|
||||
// if (existsSync(venvPath)) {
|
||||
// rmSync(venvPath, { recursive: true, force: true });
|
||||
// helpers.logger.warn("Removed broken venv directory.");
|
||||
// }
|
||||
// } catch (cleanupErr) {
|
||||
// helpers.logger.error("Error while cleaning up broken venv:", cleanupErr);
|
||||
// }
|
||||
|
||||
// throw new Error("Python venv creation failed. Check if python3 and python3-venv are installed.");
|
||||
// }
|
||||
// } else {
|
||||
// helpers.logger.info("Using existing Python venv.");
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
async function ffprobe(videoPath: string): Promise<{ fps: number; frames: number }> {
|
||||
const spawn = await getNanoSpawn()
|
||||
const { stdout } = await spawn('ffprobe', [
|
||||
'-v', 'error',
|
||||
'-select_streams', 'v:0',
|
||||
'-count_frames',
|
||||
'-show_entries', 'stream=nb_read_frames,r_frame_rate',
|
||||
'-of', 'default=nokey=1:noprint_wrappers=1',
|
||||
videoPath,
|
||||
])
|
||||
|
||||
const [frameRateStr, frameCountStr] = stdout.trim().split('\n')
|
||||
const [num, denom] = frameRateStr.trim().split('/').map(Number)
|
||||
const fps = num / denom
|
||||
const frames = parseInt(frameCountStr.trim(), 10)
|
||||
|
||||
return { fps, frames }
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export async function buildFunscript(
|
||||
helpers: Helpers,
|
||||
predictionOutput: string,
|
||||
videoPath: string
|
||||
): Promise<string> {
|
||||
const labelDir = join(predictionOutput, 'labels');
|
||||
const yamlPath = join(predictionOutput, 'data.yaml');
|
||||
const outputPath = join(process.env.CACHE_ROOT ?? '/tmp', `${nanoid()}.funscript`);
|
||||
helpers.logger.info('Starting Funscript generation');
|
||||
|
||||
try {
|
||||
|
||||
const data = await loadDataYaml(join(env.VIBEUI_DIR, 'data.yaml'))
|
||||
const classPositionMap = await loadClassPositionMap(data, helpers);
|
||||
const { fps, totalFrames } = await loadVideoMetadata(videoPath, helpers);
|
||||
const detectionSegments = await processLabelFiles(labelDir, helpers, data);
|
||||
const totalDurationMs = Math.floor((totalFrames / fps) * 1000);
|
||||
const actions = generateActions(totalDurationMs, fps, detectionSegments, classPositionMap);
|
||||
await writeFunscript(outputPath, actions, helpers);
|
||||
|
||||
return outputPath;
|
||||
} catch (error) {
|
||||
helpers.logger.error(`Error generating Funscript: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function inference(helpers: Helpers, videoFilePath: string): Promise<string> {
|
||||
const spawn = await getNanoSpawn()
|
||||
|
||||
const modelPath = join(env.VIBEUI_DIR, 'runs/detect/vibeui/weights/best.pt')
|
||||
|
||||
// Generate a unique name based on video name + UUID
|
||||
const videoExt = extname(videoFilePath) // e.g. '.mp4'
|
||||
const videoName = basename(videoFilePath, videoExt) // removes the extension
|
||||
const uniqueName = `${videoName}-${nanoid()}`
|
||||
const customProjectDir = 'runs' // or any custom folder
|
||||
const outputPath = join(env.VIBEUI_DIR, customProjectDir, uniqueName)
|
||||
|
||||
await spawn('./venv/bin/yolo', [
|
||||
'predict',
|
||||
`model=${modelPath}`,
|
||||
`source=${videoFilePath}`,
|
||||
'save=False',
|
||||
'save_txt=True',
|
||||
'save_conf=True',
|
||||
`project=${customProjectDir}`,
|
||||
`name=${uniqueName}`,
|
||||
], {
|
||||
cwd: env.VIBEUI_DIR,
|
||||
stdio: 'inherit',
|
||||
})
|
||||
|
||||
return outputPath // contains labels/ folder and predictions
|
||||
}
|
||||
|
||||
|
||||
async function loadClassPositionMap(data: DataYaml, helpers: Helpers): Promise<ClassPositionMap> {
|
||||
try {
|
||||
|
||||
if (
|
||||
!data ||
|
||||
typeof data !== 'object' ||
|
||||
!('names' in data) ||
|
||||
typeof data.names !== 'object' ||
|
||||
data.names === null ||
|
||||
Object.keys(data.names).length === 0
|
||||
) {
|
||||
throw new Error('Invalid data.yaml: "names" field is missing, not an object, or empty');
|
||||
}
|
||||
|
||||
const positionMap: ClassPositionMap = {
|
||||
ControlledByTipper: 50,
|
||||
ControlledByTipperHigh: 80,
|
||||
ControlledByTipperLow: 20,
|
||||
ControlledByTipperMedium: 50,
|
||||
ControlledByTipperUltrahigh: 95,
|
||||
Ring1: 30,
|
||||
Ring2: 40,
|
||||
Ring3: 50,
|
||||
Ring4: 60,
|
||||
Earthquake: 'pattern',
|
||||
Fireworks: 'pattern',
|
||||
Pulse: 'pattern',
|
||||
Wave: 'pattern',
|
||||
Pause: 0,
|
||||
RandomTime: 70,
|
||||
HighLevel: 80,
|
||||
LowLevel: 20,
|
||||
MediumLevel: 50,
|
||||
UltraHighLevel: 95
|
||||
};
|
||||
|
||||
const names = Object.values(data.names);
|
||||
for (const name of names) {
|
||||
if (typeof name !== 'string' || name.trim() === '') {
|
||||
helpers.logger.info(`Skipping invalid class name: ${name}`);
|
||||
continue;
|
||||
}
|
||||
if (!(name in positionMap)) {
|
||||
helpers.logger.info(`No position mapping for class "${name}", defaulting to 0`);
|
||||
positionMap[name] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
helpers.logger.info(`Loaded class position map: ${JSON.stringify(positionMap)}`);
|
||||
return positionMap;
|
||||
} catch (error) {
|
||||
helpers.logger.error(`Error loading data.yaml: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function generatePatternPositions(startMs: number, durationMs: number, className: string, fps: number): FunscriptAction[] {
|
||||
const actions: FunscriptAction[] = [];
|
||||
const frameDurationMs = 1000 / fps;
|
||||
const totalFrames = Math.floor(durationMs / frameDurationMs);
|
||||
const intervalMs = 100;
|
||||
|
||||
for (let timeMs = 0; timeMs < durationMs; timeMs += intervalMs) {
|
||||
const progress = timeMs / durationMs;
|
||||
let pos = 0;
|
||||
|
||||
switch (className) {
|
||||
case 'Pulse':
|
||||
pos = Math.round(50 * Math.sin(progress * 2 * Math.PI));
|
||||
break;
|
||||
case 'Wave':
|
||||
pos = Math.round(50 + 50 * Math.sin(progress * 2 * Math.PI));
|
||||
break;
|
||||
case 'Fireworks':
|
||||
pos = Math.random() > 0.5 ? 80 : 0;
|
||||
break;
|
||||
case 'Earthquake':
|
||||
pos = Math.round(90 * Math.sin(progress * 4 * Math.PI) + (Math.random() - 0.5) * 10);
|
||||
pos = Math.max(0, Math.min(90, pos));
|
||||
break;
|
||||
}
|
||||
|
||||
actions.push({ at: startMs + timeMs, pos });
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
async function loadVideoMetadata(videoPath: string, helpers: Helpers) {
|
||||
const { fps, frames: totalFrames } = await ffprobe(videoPath);
|
||||
helpers.logger.info(`Video metadata: fps=${fps}, frames=${totalFrames}`);
|
||||
return { fps, totalFrames };
|
||||
}
|
||||
|
||||
async function processLabelFiles(labelDir: string, helpers: Helpers, data: DataYaml): Promise<Detection[]> {
|
||||
const labelFiles = (await readdir(labelDir)).filter(file => file.endsWith('.txt'));
|
||||
const detections: Map<number, Detection[]> = new Map();
|
||||
const names = data.names;
|
||||
|
||||
for (const file of labelFiles) {
|
||||
const match = file.match(/(\d+)\.txt$/);
|
||||
if (!match) {
|
||||
helpers.logger.info(`Skipping invalid filename: ${file}`);
|
||||
continue;
|
||||
}
|
||||
const frameIndex = parseInt(match[1], 10);
|
||||
if (isNaN(frameIndex)) {
|
||||
helpers.logger.info(`Skipping invalid frame index from filename: ${file}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const content = await readFile(join(labelDir, file), 'utf8');
|
||||
const lines = content.trim().split('\n');
|
||||
const frameDetections: Detection[] = [];
|
||||
let maxConfidence = 0;
|
||||
let selectedClassIndex = -1;
|
||||
|
||||
for (const line of lines) {
|
||||
const parts = line.trim().split(/\s+/);
|
||||
if (parts.length < 6) continue;
|
||||
|
||||
const classIndex = parseInt(parts[0], 10);
|
||||
const confidence = parseFloat(parts[5]);
|
||||
if (isNaN(classIndex) || isNaN(confidence)) continue;
|
||||
|
||||
if (confidence >= 0.7 && confidence > maxConfidence) {
|
||||
maxConfidence = confidence;
|
||||
selectedClassIndex = classIndex;
|
||||
}
|
||||
}
|
||||
|
||||
if (maxConfidence > 0) {
|
||||
const className = (data.names as Record<string, string>)[selectedClassIndex.toString()];
|
||||
if (className) {
|
||||
frameDetections.push({ startFrame: frameIndex, endFrame: frameIndex, className });
|
||||
}
|
||||
}
|
||||
|
||||
if (frameDetections.length > 0) {
|
||||
detections.set(frameIndex, frameDetections);
|
||||
}
|
||||
}
|
||||
|
||||
// Merge overlapping detections into continuous segments
|
||||
const detectionSegments: Detection[] = [];
|
||||
let currentDetection: Detection | null = null;
|
||||
|
||||
for (const [frameIndex, frameDetections] of detections.entries()) {
|
||||
for (const detection of frameDetections) {
|
||||
if (!currentDetection || currentDetection.className !== detection.className) {
|
||||
if (currentDetection) detectionSegments.push(currentDetection);
|
||||
currentDetection = { ...detection, endFrame: frameIndex };
|
||||
} else {
|
||||
currentDetection.endFrame = frameIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (currentDetection) detectionSegments.push(currentDetection);
|
||||
|
||||
return detectionSegments;
|
||||
}
|
||||
|
||||
function generateActions(totalDurationMs: number, fps: number, detectionSegments: Detection[], classPositionMap: ClassPositionMap): FunscriptAction[] {
|
||||
const intervalMs = 100;
|
||||
const actions: FunscriptAction[] = [];
|
||||
|
||||
// Generate static position actions
|
||||
for (let timeMs = 0; timeMs <= totalDurationMs; timeMs += intervalMs) {
|
||||
const frameIndex = Math.floor((timeMs / 1000) * fps);
|
||||
let position = 0;
|
||||
|
||||
for (const segment of detectionSegments) {
|
||||
if (frameIndex >= segment.startFrame && frameIndex <= segment.endFrame) {
|
||||
const className = segment.className;
|
||||
if (typeof classPositionMap[className] === 'number') {
|
||||
position = classPositionMap[className];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
actions.push({ at: timeMs, pos: position });
|
||||
}
|
||||
|
||||
// Overlay pattern-based actions
|
||||
for (const segment of detectionSegments) {
|
||||
const className = segment.className;
|
||||
if (classPositionMap[className] === 'pattern') {
|
||||
const startMs = Math.floor((segment.startFrame / fps) * 1000);
|
||||
const durationMs = Math.floor(((segment.endFrame - segment.startFrame + 1) / fps) * 1000);
|
||||
const patternActions = generatePatternPositions(startMs, durationMs, className, fps);
|
||||
actions.push(...patternActions);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort actions by time and remove duplicates
|
||||
actions.sort((a, b) => a.at - b.at);
|
||||
const uniqueActions: FunscriptAction[] = [];
|
||||
let lastTime = -1;
|
||||
for (const action of actions) {
|
||||
if (action.at !== lastTime) {
|
||||
uniqueActions.push(action);
|
||||
lastTime = action.at;
|
||||
}
|
||||
}
|
||||
|
||||
return uniqueActions;
|
||||
}
|
||||
|
||||
async function writeFunscript(outputPath: string, actions: FunscriptAction[], helpers: Helpers) {
|
||||
const funscript: Funscript = { version: '1.0', actions };
|
||||
await writeFile(outputPath, JSON.stringify(funscript, null, 2));
|
||||
helpers.logger.info(`Funscript generated: ${outputPath} (${actions.length} actions)`);
|
||||
}
|
||||
|
||||
|
||||
const createFunscript: Task = async (payload: any, helpers: Helpers) => {
|
||||
assertPayload(payload);
|
||||
const { vodId } = payload;
|
||||
|
||||
|
||||
|
||||
const vod = await prisma.vod.findFirstOrThrow({ where: { id: vodId } });
|
||||
await preparePython(helpers)
|
||||
|
||||
if (vod.funscript) {
|
||||
helpers.logger.info(`Doing nothing-- vod ${vodId} already has a funscript.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!vod.sourceVideo) {
|
||||
const msg = `Cannot create funscript: Vod ${vodId} is missing a source video.`;
|
||||
helpers.logger.warn(msg);
|
||||
throw new Error(msg);
|
||||
}
|
||||
|
||||
|
||||
|
||||
const s3Client = getS3Client();
|
||||
const videoFilePath = await getOrDownloadAsset(s3Client, env.S3_BUCKET, vod.sourceVideo);
|
||||
helpers.logger.info(`Downloaded video to ${videoFilePath}`);
|
||||
|
||||
helpers.logger.info(`Creating funscript for vod ${vodId}...`);
|
||||
|
||||
const predictionOutput = await inference(helpers, videoFilePath);
|
||||
helpers.logger.info(`prediction output ${predictionOutput}`);
|
||||
|
||||
|
||||
const funscriptFilePath = await buildFunscript(helpers, predictionOutput, videoFilePath)
|
||||
|
||||
|
||||
const s3Key = `funscripts/${vodId}.funscript`;
|
||||
const s3Url = await uploadFile(s3Client, env.S3_BUCKET, s3Key, funscriptFilePath, "application/json");
|
||||
|
||||
helpers.logger.info(`Uploaded funscript to S3: ${s3Url}`);
|
||||
|
||||
await prisma.vod.update({
|
||||
where: { id: vodId },
|
||||
data: { funscript: s3Key }
|
||||
});
|
||||
|
||||
helpers.logger.info(`Funscript saved to database for vod ${vodId}`);
|
||||
};
|
||||
|
||||
export default createFunscript;
|
@ -126,8 +126,6 @@ export async function packageHls(
|
||||
args.push('--segment_duration=2'); // matches Twitch’s chunk size
|
||||
|
||||
|
||||
helpers.logger.info(`PILLS HERE\nPILLS HERE\nPILLS HERE\nPILLS HERE\nPILLS HERE\nPILLS HERE\nPILLS HERE`)
|
||||
|
||||
await spawn('packager', args, {
|
||||
stdout: 'inherit',
|
||||
stderr: 'inherit',
|
||||
|
@ -1,5 +1,4 @@
|
||||
import type { Task, Helpers } from "graphile-worker";
|
||||
import spawn from 'nano-spawn';
|
||||
import { PrismaClient } from "../../generated/prisma";
|
||||
import { withAccelerate } from "@prisma/extension-accelerate";
|
||||
import { getOrDownloadAsset } from "../utils/cache";
|
||||
@ -7,6 +6,7 @@ import { env } from "../config/env";
|
||||
import { S3Client } from "@aws-sdk/client-s3";
|
||||
import { getS3Client, uploadFile } from "../utils/s3";
|
||||
import { nanoid } from "nanoid";
|
||||
import { getNanoSpawn } from "../utils/nanoSpawn";
|
||||
|
||||
const prisma = new PrismaClient().$extends(withAccelerate());
|
||||
|
||||
@ -30,7 +30,7 @@ async function hash(helpers: Helpers, inputFilePath: string) {
|
||||
throw new Error("inputFilePath is missing");
|
||||
}
|
||||
|
||||
|
||||
const spawn = await getNanoSpawn();
|
||||
const result = await spawn('ipfs', [
|
||||
'add',
|
||||
'--cid-version=1',
|
||||
|
@ -1,5 +1,4 @@
|
||||
import type { Task, Helpers } from "graphile-worker";
|
||||
import spawn from 'nano-spawn';
|
||||
import { PrismaClient } from "../../generated/prisma";
|
||||
import { withAccelerate } from "@prisma/extension-accelerate";
|
||||
import { getOrDownloadAsset } from "../utils/cache";
|
||||
@ -7,6 +6,7 @@ import { env } from "../config/env";
|
||||
import { S3Client } from "@aws-sdk/client-s3";
|
||||
import { getS3Client, uploadFile } from "../utils/s3";
|
||||
import { nanoid } from "nanoid";
|
||||
import { getNanoSpawn } from "../utils/nanoSpawn";
|
||||
|
||||
const prisma = new PrismaClient().$extends(withAccelerate());
|
||||
|
||||
@ -25,6 +25,7 @@ async function createThumbnail(helpers: Helpers, inputFilePath: string) {
|
||||
|
||||
|
||||
const outputFilePath = inputFilePath.replace(/\.[^/.]+$/, '') + '-thumb.png';
|
||||
const spawn = await getNanoSpawn();
|
||||
const result = await spawn('vcsi', [
|
||||
inputFilePath,
|
||||
'--metadata-position', 'hidden',
|
||||
|
@ -6,11 +6,11 @@ import path from "node:path";
|
||||
import { join } from "node:path";
|
||||
import { getOrDownloadAsset } from "../utils/cache";
|
||||
import { env } from "../config/env";
|
||||
import spawn from "nano-spawn";
|
||||
import { nanoid } from "nanoid";
|
||||
import { uploadFile } from "../utils/s3";
|
||||
import { S3Client } from "@aws-sdk/client-s3";
|
||||
import { VodSegment } from "../types";
|
||||
import { getNanoSpawn } from "../utils/nanoSpawn";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
const client = new S3Client({
|
||||
@ -92,6 +92,7 @@ async function concatenateSegments(
|
||||
helpers.logger.info(`Concatenating ${concatSpec.files.length} segments`);
|
||||
|
||||
try {
|
||||
const spawn = await getNanoSpawn();
|
||||
const proc = await spawn("ffmpeg", [
|
||||
"-f", "concat",
|
||||
"-safe", "0",
|
||||
|
25
services/our/src/tests/fixtures/data.yaml
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
path: /home/cj/Documents/futureporn-monorepo/apps/vibeui/data
|
||||
train: train/images
|
||||
val: val/images
|
||||
|
||||
names:
|
||||
0: ControlledByTipper
|
||||
1: ControlledByTipperHigh
|
||||
2: ControlledByTipperLow
|
||||
3: ControlledByTipperMedium
|
||||
4: ControlledByTipperUltrahigh
|
||||
5: Earthquake
|
||||
6: Fireworks
|
||||
7: HighLevel
|
||||
8: LowLevel
|
||||
9: MediumLevel
|
||||
10: Pause
|
||||
11: Pulse
|
||||
12: RandomTime
|
||||
13: RespondingTo
|
||||
14: Ring1
|
||||
15: Ring2
|
||||
16: Ring3
|
||||
17: Ring4
|
||||
18: UltraHighLevel
|
||||
19: Wave
|
BIN
services/our/src/tests/fixtures/prediction/frames/000001.jpg
vendored
Normal file
After Width: | Height: | Size: 87 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000002.jpg
vendored
Normal file
After Width: | Height: | Size: 104 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000003.jpg
vendored
Normal file
After Width: | Height: | Size: 136 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000004.jpg
vendored
Normal file
After Width: | Height: | Size: 116 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000005.jpg
vendored
Normal file
After Width: | Height: | Size: 95 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000006.jpg
vendored
Normal file
After Width: | Height: | Size: 81 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000007.jpg
vendored
Normal file
After Width: | Height: | Size: 70 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000008.jpg
vendored
Normal file
After Width: | Height: | Size: 62 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000009.jpg
vendored
Normal file
After Width: | Height: | Size: 56 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000010.jpg
vendored
Normal file
After Width: | Height: | Size: 52 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000011.jpg
vendored
Normal file
After Width: | Height: | Size: 50 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000012.jpg
vendored
Normal file
After Width: | Height: | Size: 48 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000013.jpg
vendored
Normal file
After Width: | Height: | Size: 48 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000014.jpg
vendored
Normal file
After Width: | Height: | Size: 48 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000015.jpg
vendored
Normal file
After Width: | Height: | Size: 49 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000016.jpg
vendored
Normal file
After Width: | Height: | Size: 49 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000017.jpg
vendored
Normal file
After Width: | Height: | Size: 49 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000018.jpg
vendored
Normal file
After Width: | Height: | Size: 49 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000019.jpg
vendored
Normal file
After Width: | Height: | Size: 50 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000020.jpg
vendored
Normal file
After Width: | Height: | Size: 49 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000021.jpg
vendored
Normal file
After Width: | Height: | Size: 50 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000022.jpg
vendored
Normal file
After Width: | Height: | Size: 49 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000023.jpg
vendored
Normal file
After Width: | Height: | Size: 50 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000024.jpg
vendored
Normal file
After Width: | Height: | Size: 49 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000025.jpg
vendored
Normal file
After Width: | Height: | Size: 49 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000026.jpg
vendored
Normal file
After Width: | Height: | Size: 49 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000027.jpg
vendored
Normal file
After Width: | Height: | Size: 50 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000028.jpg
vendored
Normal file
After Width: | Height: | Size: 50 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000029.jpg
vendored
Normal file
After Width: | Height: | Size: 50 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000030.jpg
vendored
Normal file
After Width: | Height: | Size: 50 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000031.jpg
vendored
Normal file
After Width: | Height: | Size: 50 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000032.jpg
vendored
Normal file
After Width: | Height: | Size: 50 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000033.jpg
vendored
Normal file
After Width: | Height: | Size: 50 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000034.jpg
vendored
Normal file
After Width: | Height: | Size: 50 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000035.jpg
vendored
Normal file
After Width: | Height: | Size: 69 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000036.jpg
vendored
Normal file
After Width: | Height: | Size: 69 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000037.jpg
vendored
Normal file
After Width: | Height: | Size: 69 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000038.jpg
vendored
Normal file
After Width: | Height: | Size: 70 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000039.jpg
vendored
Normal file
After Width: | Height: | Size: 70 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000040.jpg
vendored
Normal file
After Width: | Height: | Size: 69 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000041.jpg
vendored
Normal file
After Width: | Height: | Size: 69 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000042.jpg
vendored
Normal file
After Width: | Height: | Size: 69 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000043.jpg
vendored
Normal file
After Width: | Height: | Size: 69 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000044.jpg
vendored
Normal file
After Width: | Height: | Size: 69 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000045.jpg
vendored
Normal file
After Width: | Height: | Size: 69 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000046.jpg
vendored
Normal file
After Width: | Height: | Size: 69 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000047.jpg
vendored
Normal file
After Width: | Height: | Size: 70 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000048.jpg
vendored
Normal file
After Width: | Height: | Size: 69 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000049.jpg
vendored
Normal file
After Width: | Height: | Size: 69 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000050.jpg
vendored
Normal file
After Width: | Height: | Size: 69 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000051.jpg
vendored
Normal file
After Width: | Height: | Size: 69 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000052.jpg
vendored
Normal file
After Width: | Height: | Size: 69 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000053.jpg
vendored
Normal file
After Width: | Height: | Size: 69 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000054.jpg
vendored
Normal file
After Width: | Height: | Size: 69 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000055.jpg
vendored
Normal file
After Width: | Height: | Size: 69 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000056.jpg
vendored
Normal file
After Width: | Height: | Size: 69 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000057.jpg
vendored
Normal file
After Width: | Height: | Size: 69 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000058.jpg
vendored
Normal file
After Width: | Height: | Size: 52 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000059.jpg
vendored
Normal file
After Width: | Height: | Size: 50 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000060.jpg
vendored
Normal file
After Width: | Height: | Size: 49 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000061.jpg
vendored
Normal file
After Width: | Height: | Size: 47 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000062.jpg
vendored
Normal file
After Width: | Height: | Size: 46 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000063.jpg
vendored
Normal file
After Width: | Height: | Size: 47 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000064.jpg
vendored
Normal file
After Width: | Height: | Size: 50 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000065.jpg
vendored
Normal file
After Width: | Height: | Size: 53 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000066.jpg
vendored
Normal file
After Width: | Height: | Size: 54 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000067.jpg
vendored
Normal file
After Width: | Height: | Size: 54 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000068.jpg
vendored
Normal file
After Width: | Height: | Size: 53 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000069.jpg
vendored
Normal file
After Width: | Height: | Size: 52 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000070.jpg
vendored
Normal file
After Width: | Height: | Size: 56 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000071.jpg
vendored
Normal file
After Width: | Height: | Size: 56 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000072.jpg
vendored
Normal file
After Width: | Height: | Size: 56 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000073.jpg
vendored
Normal file
After Width: | Height: | Size: 58 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000074.jpg
vendored
Normal file
After Width: | Height: | Size: 58 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000075.jpg
vendored
Normal file
After Width: | Height: | Size: 58 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000076.jpg
vendored
Normal file
After Width: | Height: | Size: 58 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000077.jpg
vendored
Normal file
After Width: | Height: | Size: 58 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000078.jpg
vendored
Normal file
After Width: | Height: | Size: 58 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000079.jpg
vendored
Normal file
After Width: | Height: | Size: 58 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000080.jpg
vendored
Normal file
After Width: | Height: | Size: 58 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000081.jpg
vendored
Normal file
After Width: | Height: | Size: 58 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000082.jpg
vendored
Normal file
After Width: | Height: | Size: 58 KiB |
BIN
services/our/src/tests/fixtures/prediction/frames/000083.jpg
vendored
Normal file
After Width: | Height: | Size: 51 KiB |