Compare commits
9 Commits
d5632b009a
...
v1.8.13
| Author | SHA1 | Date | |
|---|---|---|---|
| ada1e9c717 | |||
| 4d295d10d1 | |||
| c00f4e5ea5 | |||
| 5f7a254fcb | |||
| 21c0c778f9 | |||
| 4f6d62a85c | |||
| 7d9604a65a | |||
| b3d089ac6d | |||
| baecc9c83c |
@@ -1,14 +1,28 @@
|
||||
node_modules
|
||||
**/node_modules
|
||||
.next
|
||||
**/.next
|
||||
.git
|
||||
# .npmrc is allowed as it contains the registry template
|
||||
dist
|
||||
**/dist
|
||||
build
|
||||
**/build
|
||||
out
|
||||
**/out
|
||||
coverage
|
||||
**/coverage
|
||||
.vercel
|
||||
**/.vercel
|
||||
.turbo
|
||||
**/.turbo
|
||||
*.log
|
||||
**/*.log
|
||||
.DS_Store
|
||||
**/.DS_Store
|
||||
.pnpm-store
|
||||
**/.pnpm-store
|
||||
.gitea
|
||||
**/.gitea
|
||||
models
|
||||
**/models
|
||||
|
||||
2
.env
2
.env
@@ -1,5 +1,5 @@
|
||||
# Project
|
||||
IMAGE_TAG=v1.8.10
|
||||
IMAGE_TAG=v1.8.12
|
||||
PROJECT_NAME=at-mintel
|
||||
PROJECT_COLOR=#82ed20
|
||||
GITEA_TOKEN=ccce002e30fe16a31a6c9d5a414740af2f72a582
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Project
|
||||
IMAGE_TAG=v1.8.10
|
||||
IMAGE_TAG=v1.8.12
|
||||
PROJECT_NAME=sample-website
|
||||
PROJECT_COLOR=#82ed20
|
||||
|
||||
|
||||
@@ -192,6 +192,9 @@ jobs:
|
||||
- image: directus
|
||||
file: packages/infra/docker/Dockerfile.directus
|
||||
name: Directus (Base)
|
||||
- image: image-processor
|
||||
file: apps/image-service/Dockerfile
|
||||
name: Image Processor
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
43
apps/image-service/Dockerfile
Normal file
43
apps/image-service/Dockerfile
Normal file
@@ -0,0 +1,43 @@
|
||||
FROM node:20.18-bookworm-slim AS base
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN npm install -g pnpm@10.30.1
|
||||
|
||||
FROM base AS build
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
# Note: Canvas needs build tools on Debian
|
||||
RUN apt-get update && apt-get install -y python3 make g++ libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev
|
||||
# Delete the prebuilt binary and force a clean rebuild from source for the correct container architecture
|
||||
RUN pnpm install --frozen-lockfile
|
||||
RUN for dir in $(find /app/node_modules -type d -path "*/@tensorflow/tfjs-node"); do \
|
||||
cd $dir && \
|
||||
rm -rf lib/napi-v8/* && \
|
||||
npm run install -- build-addon-from-source; \
|
||||
done
|
||||
# Generate models explicitly for Docker
|
||||
RUN ls -la packages/image-processor/scripts || true
|
||||
RUN pnpm dlx tsx packages/image-processor/scripts/download-models.ts
|
||||
RUN pnpm --filter @mintel/image-processor build
|
||||
RUN pnpm --filter image-service build
|
||||
# Generated locally for caching
|
||||
|
||||
FROM base
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/node_modules ./node_modules
|
||||
COPY --from=build /app/apps/image-service/node_modules ./apps/image-service/node_modules
|
||||
COPY --from=build /app/packages/image-processor/node_modules ./packages/image-processor/node_modules
|
||||
# Make sure directories exist to prevent COPY errors
|
||||
RUN mkdir -p /app/packages/image-processor/models /app/apps/image-service/dist
|
||||
COPY --from=build /app/apps/image-service/dist ./apps/image-service/dist
|
||||
COPY --from=build /app/apps/image-service/package.json ./apps/image-service/package.json
|
||||
COPY --from=build /app/packages/image-processor/dist ./packages/image-processor/dist
|
||||
COPY --from=build /app/packages/image-processor/package.json ./packages/image-processor/package.json
|
||||
COPY --from=build /app/packages/image-processor/models ./packages/image-processor/models
|
||||
|
||||
# Need runtime dependencies for canvas/sharp on Debian
|
||||
RUN apt-get update && apt-get install -y libcairo2 libpango-1.0-0 libjpeg62-turbo libgif7 librsvg2-2 && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
EXPOSE 8080
|
||||
WORKDIR /app/apps/image-service
|
||||
CMD ["npm", "run", "start"]
|
||||
23
apps/image-service/package.json
Normal file
23
apps/image-service/package.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "image-service",
|
||||
"version": "1.8.12",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "tsx watch src/index.ts",
|
||||
"build": "tsc",
|
||||
"start": "node dist/index.js",
|
||||
"lint": "eslint src"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mintel/image-processor": "workspace:*",
|
||||
"fastify": "^4.26.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@mintel/eslint-config": "workspace:*",
|
||||
"@mintel/tsconfig": "workspace:*",
|
||||
"@types/node": "^20.0.0",
|
||||
"tsx": "^4.7.1",
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
}
|
||||
80
apps/image-service/src/index.ts
Normal file
80
apps/image-service/src/index.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import Fastify from "fastify";
|
||||
import { processImageWithSmartCrop } from "@mintel/image-processor";
|
||||
|
||||
const fastify = Fastify({
|
||||
logger: true,
|
||||
});
|
||||
|
||||
fastify.get("/unsafe/:options/:urlSafeB64", async (request, reply) => {
|
||||
// Compatibility endpoint for old imgproxy calls (optional, but requested by some systems sometimes)
|
||||
// For now, replacing logic in clients is preferred. So we just redirect or error.
|
||||
return reply
|
||||
.status(400)
|
||||
.send({ error: "Legacy imgproxy API not supported. Use /process" });
|
||||
});
|
||||
|
||||
fastify.get("/process", async (request, reply) => {
|
||||
const query = request.query as {
|
||||
url?: string;
|
||||
w?: string;
|
||||
h?: string;
|
||||
q?: string;
|
||||
format?: string;
|
||||
};
|
||||
|
||||
const { url } = query;
|
||||
const width = parseInt(query.w || "800", 10);
|
||||
const height = parseInt(query.h || "600", 10);
|
||||
const quality = parseInt(query.q || "80", 10);
|
||||
const format = (query.format || "webp") as "webp" | "jpeg" | "png" | "avif";
|
||||
|
||||
if (!url) {
|
||||
return reply.status(400).send({ error: 'Parameter "url" is required' });
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
return reply
|
||||
.status(response.status)
|
||||
.send({
|
||||
error: `Failed to fetch source image: ${response.statusText}`,
|
||||
});
|
||||
}
|
||||
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const buffer = Buffer.from(arrayBuffer);
|
||||
|
||||
const processedBuffer = await processImageWithSmartCrop(buffer, {
|
||||
width,
|
||||
height,
|
||||
format,
|
||||
quality,
|
||||
});
|
||||
|
||||
reply.header("Content-Type", `image/${format}`);
|
||||
reply.header("Cache-Control", "public, max-age=31536000, immutable");
|
||||
return reply.send(processedBuffer);
|
||||
} catch (err) {
|
||||
fastify.log.error(err);
|
||||
return reply
|
||||
.status(500)
|
||||
.send({ error: "Internal Server Error processing image" });
|
||||
}
|
||||
});
|
||||
|
||||
fastify.get("/health", async () => {
|
||||
return { status: "ok" };
|
||||
});
|
||||
|
||||
const start = async () => {
|
||||
try {
|
||||
await fastify.listen({ port: 8080, host: "0.0.0.0" });
|
||||
console.log(`Server listening on 8080`);
|
||||
} catch (err) {
|
||||
fastify.log.error(err);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
start();
|
||||
11
apps/image-service/tsconfig.json
Normal file
11
apps/image-service/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "@mintel/tsconfig/base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"noEmit": false
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
@@ -1,6 +1,13 @@
|
||||
import mintelNextConfig from "@mintel/next-config";
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {};
|
||||
const nextConfig = {
|
||||
serverExternalPackages: [
|
||||
"@mintel/image-processor",
|
||||
"@tensorflow/tfjs-node",
|
||||
"sharp",
|
||||
"canvas",
|
||||
],
|
||||
};
|
||||
|
||||
export default mintelNextConfig(nextConfig);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sample-website",
|
||||
"version": "1.8.10",
|
||||
"version": "1.8.12",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -18,6 +18,7 @@
|
||||
"@mintel/next-utils": "workspace:*",
|
||||
"@mintel/observability": "workspace:*",
|
||||
"@mintel/next-observability": "workspace:*",
|
||||
"@mintel/image-processor": "workspace:*",
|
||||
"@sentry/nextjs": "10.38.0",
|
||||
"next": "16.1.6",
|
||||
"next-intl": "^4.8.2",
|
||||
|
||||
60
apps/sample-website/src/app/api/image/route.ts
Normal file
60
apps/sample-website/src/app/api/image/route.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
export const runtime = "nodejs";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const url = searchParams.get("url");
|
||||
const width = parseInt(searchParams.get("w") || "800");
|
||||
const height = parseInt(searchParams.get("h") || "600");
|
||||
const q = parseInt(searchParams.get("q") || "80");
|
||||
|
||||
if (!url) {
|
||||
return NextResponse.json(
|
||||
{ error: "Missing url parameter" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. Fetch image from original URL
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to fetch original image" },
|
||||
{ status: response.status },
|
||||
);
|
||||
}
|
||||
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const buffer = Buffer.from(arrayBuffer);
|
||||
|
||||
// Dynamically import to prevent Next.js from trying to bundle tfjs-node/sharp locally at build time
|
||||
const { processImageWithSmartCrop } =
|
||||
await import("@mintel/image-processor");
|
||||
|
||||
// 2. Process image with Face-API and Sharp
|
||||
const processedBuffer = await processImageWithSmartCrop(buffer, {
|
||||
width,
|
||||
height,
|
||||
format: "webp",
|
||||
quality: q,
|
||||
});
|
||||
|
||||
// 3. Return the processed image
|
||||
return new NextResponse(new Uint8Array(processedBuffer), {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "image/webp",
|
||||
"Cache-Control": "public, max-age=31536000, immutable",
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Image Processing Error:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to process image" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
1
models/tiny_face_detector_model-shard1
Normal file
1
models/tiny_face_detector_model-shard1
Normal file
@@ -0,0 +1 @@
|
||||
404: Not Found
|
||||
30
models/tiny_face_detector_model-weights_manifest.json
Normal file
30
models/tiny_face_detector_model-weights_manifest.json
Normal file
@@ -0,0 +1,30 @@
|
||||
[
|
||||
{
|
||||
"weights":
|
||||
[
|
||||
{"name":"conv0/filters","shape":[3,3,3,16],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.009007044399485869,"min":-1.2069439495311063}},
|
||||
{"name":"conv0/bias","shape":[16],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.005263455241334205,"min":-0.9211046672334858}},
|
||||
{"name":"conv1/depthwise_filter","shape":[3,3,16,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.004001977630690033,"min":-0.5042491814669441}},
|
||||
{"name":"conv1/pointwise_filter","shape":[1,1,16,32],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.013836609615999109,"min":-1.411334180831909}},
|
||||
{"name":"conv1/bias","shape":[32],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0015159862590771096,"min":-0.30926119685173037}},
|
||||
{"name":"conv2/depthwise_filter","shape":[3,3,32,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.002666276225856706,"min":-0.317286870876948}},
|
||||
{"name":"conv2/pointwise_filter","shape":[1,1,32,64],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.015265831292844286,"min":-1.6792414422128714}},
|
||||
{"name":"conv2/bias","shape":[64],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0020280554598453,"min":-0.37113414915168985}},
|
||||
{"name":"conv3/depthwise_filter","shape":[3,3,64,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.006100742489683862,"min":-0.8907084034938438}},
|
||||
{"name":"conv3/pointwise_filter","shape":[1,1,64,128],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.016276211832083907,"min":-2.0508026908425725}},
|
||||
{"name":"conv3/bias","shape":[128],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.003394414279975143,"min":-0.7637432129944072}},
|
||||
{"name":"conv4/depthwise_filter","shape":[3,3,128,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.006716050119961009,"min":-0.8059260143953211}},
|
||||
{"name":"conv4/pointwise_filter","shape":[1,1,128,256],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.021875603993733724,"min":-2.8875797271728514}},
|
||||
{"name":"conv4/bias","shape":[256],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0041141652009066415,"min":-0.8187188749804216}},
|
||||
{"name":"conv5/depthwise_filter","shape":[3,3,256,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.008423839597141042,"min":-0.9013508368940915}},
|
||||
{"name":"conv5/pointwise_filter","shape":[1,1,256,512],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.030007277283014035,"min":-3.8709387695088107}},
|
||||
{"name":"conv5/bias","shape":[512],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.008402082966823203,"min":-1.4871686851277068}},
|
||||
{"name":"conv8/filters","shape":[1,1,512,25],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.028336129469030042,"min":-4.675461362389957}},
|
||||
{"name":"conv8/bias","shape":[25],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.002268134028303857,"min":-0.41053225912299807}}
|
||||
],
|
||||
"paths":
|
||||
[
|
||||
"tiny_face_detector_model.bin"
|
||||
]
|
||||
}
|
||||
]
|
||||
16
package.json
16
package.json
@@ -57,11 +57,23 @@
|
||||
"pino-pretty": "^13.1.3",
|
||||
"require-in-the-middle": "^8.0.1"
|
||||
},
|
||||
"version": "1.8.10",
|
||||
"version": "1.8.12",
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"@parcel/watcher",
|
||||
"@sentry/cli",
|
||||
"@swc/core",
|
||||
"@tensorflow/tfjs-node",
|
||||
"canvas",
|
||||
"core-js",
|
||||
"esbuild",
|
||||
"sharp",
|
||||
"unrs-resolver",
|
||||
"vue-demi"
|
||||
],
|
||||
"overrides": {
|
||||
"next": "16.1.6",
|
||||
"@sentry/nextjs": "10.38.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "acquisition-manager",
|
||||
"description": "Custom High-Fidelity Management for Directus",
|
||||
"icon": "extension",
|
||||
"version": "1.8.10",
|
||||
"version": "1.8.12",
|
||||
"type": "module",
|
||||
"keywords": [
|
||||
"directus",
|
||||
@@ -27,4 +27,4 @@
|
||||
"@directus/extensions-sdk": "11.0.2",
|
||||
"vue": "^3.4.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "acquisition",
|
||||
"version": "1.8.10",
|
||||
"version": "1.8.12",
|
||||
"type": "module",
|
||||
"directus:extension": {
|
||||
"type": "endpoint",
|
||||
@@ -24,4 +24,4 @@
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mintel/cli",
|
||||
"version": "1.8.10",
|
||||
"version": "1.8.12",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://npm.infra.mintel.me"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mintel/cloner",
|
||||
"version": "1.8.10",
|
||||
"version": "1.8.12",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.js",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mintel/cms-infra",
|
||||
"version": "1.8.10",
|
||||
"version": "1.8.12",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -15,4 +15,4 @@
|
||||
"sync:push": "../../scripts/sync-directus.sh push infra",
|
||||
"sync:pull": "../../scripts/sync-directus.sh pull infra"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "company-manager",
|
||||
"description": "Custom High-Fidelity Management for Directus",
|
||||
"icon": "extension",
|
||||
"version": "1.8.10",
|
||||
"version": "1.8.12",
|
||||
"type": "module",
|
||||
"keywords": [
|
||||
"directus",
|
||||
@@ -27,4 +27,4 @@
|
||||
"@directus/extensions-sdk": "11.0.2",
|
||||
"vue": "^3.4.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mintel/content-engine",
|
||||
"version": "1.0.0",
|
||||
"version": "1.8.12",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
|
||||
@@ -475,6 +475,7 @@ BLOG POST BEST PRACTICES (MANDATORY):
|
||||
- Agiere als hochprofessioneller Digital Architect und entferne alte MDX-Metadaten im Body.
|
||||
- Fazit: Schließe JEDEN Artikel ZWINGEND mit einem starken, klaren 'Fazit' ab.
|
||||
- ORIGINAL LANGUAGE QUOTES: Übersetze NIEMALS Zitate (z.B. in ArticleQuote). Behalte das Original (z.B. Englisch), wenn du Studien von Deloitte, McKinsey oder Aussagen von CEOs zitierst. Das erhöht die Authentizität im B2B-Mittelstand.
|
||||
- CONTENT PRUNING: Wenn das dir übergebene MDX bereits interaktive Komponenten (z.B. \`<YouTubeEmbed>\`) enthält, die **nicht** oder **nicht mehr** zum inhaltlichen Fokus passen (z.B. irrelevante Videos oder platzhalter-ähnliche Snippets), MUSST du diese radikal **entfernen**. Behalte keine halluzinierten oder unpassenden Medien, nur weil sie schon da waren.
|
||||
|
||||
STRICT MDX OUTPUT RULES:
|
||||
1. ONLY use the exact components defined above.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "customer-manager",
|
||||
"description": "Custom High-Fidelity Management for Directus",
|
||||
"icon": "extension",
|
||||
"version": "1.8.10",
|
||||
"version": "1.8.12",
|
||||
"type": "module",
|
||||
"keywords": [
|
||||
"directus",
|
||||
@@ -27,4 +27,4 @@
|
||||
"@directus/extensions-sdk": "11.0.2",
|
||||
"vue": "^3.4.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mintel/directus-extension-toolkit",
|
||||
"version": "1.8.10",
|
||||
"version": "1.8.12",
|
||||
"description": "Shared toolkit for Directus extensions in the Mintel ecosystem",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mintel/eslint-config",
|
||||
"version": "1.8.10",
|
||||
"version": "1.8.12",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://npm.infra.mintel.me"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "feedback-commander",
|
||||
"description": "Custom High-Fidelity Management for Directus",
|
||||
"icon": "extension",
|
||||
"version": "1.8.10",
|
||||
"version": "1.8.12",
|
||||
"type": "module",
|
||||
"keywords": [
|
||||
"directus",
|
||||
@@ -24,4 +24,4 @@
|
||||
"@directus/extensions-sdk": "11.0.2",
|
||||
"vue": "^3.4.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mintel/gatekeeper",
|
||||
"version": "1.8.10",
|
||||
"version": "1.8.12",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mintel/husky-config",
|
||||
"version": "1.8.10",
|
||||
"version": "1.8.12",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://npm.infra.mintel.me"
|
||||
|
||||
33
packages/image-processor/package.json
Normal file
33
packages/image-processor/package.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "@mintel/image-processor",
|
||||
"version": "1.8.12",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsup src/index.ts --format esm --dts --clean",
|
||||
"dev": "tsup src/index.ts --format esm --watch --dts",
|
||||
"lint": "eslint src"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tensorflow/tfjs-node": "^4.22.0",
|
||||
"@vladmandic/face-api": "^1.7.13",
|
||||
"canvas": "^2.11.2",
|
||||
"sharp": "^0.33.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@mintel/eslint-config": "workspace:*",
|
||||
"@mintel/tsconfig": "workspace:*",
|
||||
"@types/node": "^20.0.0",
|
||||
"tsup": "^8.3.5",
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
}
|
||||
55
packages/image-processor/scripts/download-models.ts
Normal file
55
packages/image-processor/scripts/download-models.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import * as https from "node:https";
|
||||
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const MODELS_DIR = path.join(__dirname, "..", "models");
|
||||
const BASE_URL =
|
||||
"https://raw.githubusercontent.com/vladmandic/face-api/master/model/";
|
||||
|
||||
const models = [
|
||||
"tiny_face_detector_model-weights_manifest.json",
|
||||
"tiny_face_detector_model-shard1",
|
||||
];
|
||||
|
||||
async function downloadModel(filename: string) {
|
||||
const destPath = path.join(MODELS_DIR, filename);
|
||||
if (fs.existsSync(destPath)) {
|
||||
console.log(`Model ${filename} already exists.`);
|
||||
return;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log(`Downloading ${filename}...`);
|
||||
const file = fs.createWriteStream(destPath);
|
||||
https
|
||||
.get(BASE_URL + filename, (response) => {
|
||||
response.pipe(file);
|
||||
file.on("finish", () => {
|
||||
file.close();
|
||||
resolve(true);
|
||||
});
|
||||
})
|
||||
.on("error", (err) => {
|
||||
fs.unlinkSync(destPath);
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (!fs.existsSync(MODELS_DIR)) {
|
||||
fs.mkdirSync(MODELS_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
for (const model of models) {
|
||||
await downloadModel(model);
|
||||
}
|
||||
|
||||
console.log("All models downloaded successfully!");
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
1
packages/image-processor/src/index.ts
Normal file
1
packages/image-processor/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './processor.js';
|
||||
140
packages/image-processor/src/processor.ts
Normal file
140
packages/image-processor/src/processor.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import * as faceapi from "@vladmandic/face-api";
|
||||
// Provide Canvas fallback for face-api in Node.js
|
||||
import { Canvas, Image, ImageData } from "canvas";
|
||||
import sharp from "sharp";
|
||||
import * as path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
// @ts-expect-error FaceAPI does not have type definitions for monkeyPatch
|
||||
faceapi.env.monkeyPatch({ Canvas, Image, ImageData });
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
// Path to the downloaded models
|
||||
const MODELS_PATH = path.join(__dirname, "..", "models");
|
||||
|
||||
let isModelsLoaded = false;
|
||||
|
||||
async function loadModels() {
|
||||
if (isModelsLoaded) return;
|
||||
await faceapi.nets.tinyFaceDetector.loadFromDisk(MODELS_PATH);
|
||||
isModelsLoaded = true;
|
||||
}
|
||||
|
||||
export interface ProcessImageOptions {
|
||||
width: number;
|
||||
height: number;
|
||||
format?: "webp" | "jpeg" | "png" | "avif";
|
||||
quality?: number;
|
||||
}
|
||||
|
||||
export async function processImageWithSmartCrop(
|
||||
inputBuffer: Buffer,
|
||||
options: ProcessImageOptions,
|
||||
): Promise<Buffer> {
|
||||
await loadModels();
|
||||
|
||||
// Load image via Canvas for face-api
|
||||
const img = new Image();
|
||||
img.src = inputBuffer;
|
||||
|
||||
// Detect faces
|
||||
const detections = await faceapi.detectAllFaces(
|
||||
// @ts-expect-error FaceAPI does not have type definitions for monkeyPatch
|
||||
img,
|
||||
new faceapi.TinyFaceDetectorOptions(),
|
||||
);
|
||||
|
||||
const sharpImage = sharp(inputBuffer);
|
||||
const metadata = await sharpImage.metadata();
|
||||
|
||||
if (!metadata.width || !metadata.height) {
|
||||
throw new Error("Could not read image metadata");
|
||||
}
|
||||
|
||||
// If faces are found, calculate the bounding box containing all faces
|
||||
if (detections.length > 0) {
|
||||
let minX = metadata.width;
|
||||
let minY = metadata.height;
|
||||
let maxX = 0;
|
||||
let maxY = 0;
|
||||
|
||||
for (const det of detections) {
|
||||
const { x, y, width, height } = det.box;
|
||||
if (x < minX) minX = Math.max(0, x);
|
||||
if (y < minY) minY = Math.max(0, y);
|
||||
if (x + width > maxX) maxX = Math.min(metadata.width, x + width);
|
||||
if (y + height > maxY) maxY = Math.min(metadata.height, y + height);
|
||||
}
|
||||
|
||||
const faceBoxWidth = maxX - minX;
|
||||
const faceBoxHeight = maxY - minY;
|
||||
|
||||
// Calculate center of the faces
|
||||
const centerX = Math.floor(minX + faceBoxWidth / 2);
|
||||
const centerY = Math.floor(minY + faceBoxHeight / 2);
|
||||
|
||||
// Provide this as a focus point for sharp's extract or resize
|
||||
// We can use sharp's resize with `position` focusing on crop options,
|
||||
// or calculate an exact bounding box. However, extracting an exact bounding box
|
||||
// and then resizing usually yields the best results when focusing on a specific coordinate.
|
||||
|
||||
// A simpler approach is to crop a rectangle with the target aspect ratio
|
||||
// centered on the faces, then resize. Let's calculate the crop box.
|
||||
|
||||
const targetRatio = options.width / options.height;
|
||||
const currentRatio = metadata.width / metadata.height;
|
||||
|
||||
let cropWidth = metadata.width;
|
||||
let cropHeight = metadata.height;
|
||||
|
||||
if (currentRatio > targetRatio) {
|
||||
// Image is wider than target, calculate new width
|
||||
cropWidth = Math.floor(metadata.height * targetRatio);
|
||||
} else {
|
||||
// Image is taller than target, calculate new height
|
||||
cropHeight = Math.floor(metadata.width / targetRatio);
|
||||
}
|
||||
|
||||
// Try to center the crop box around the faces
|
||||
let cropX = Math.floor(centerX - cropWidth / 2);
|
||||
let cropY = Math.floor(centerY - cropHeight / 2);
|
||||
|
||||
// Keep crop box within image bounds
|
||||
if (cropX < 0) cropX = 0;
|
||||
if (cropY < 0) cropY = 0;
|
||||
if (cropX + cropWidth > metadata.width) cropX = metadata.width - cropWidth;
|
||||
if (cropY + cropHeight > metadata.height)
|
||||
cropY = metadata.height - cropHeight;
|
||||
|
||||
sharpImage.extract({
|
||||
left: cropX,
|
||||
top: cropY,
|
||||
width: cropWidth,
|
||||
height: cropHeight,
|
||||
});
|
||||
}
|
||||
|
||||
// Finally, resize to the requested dimensions and format
|
||||
let finalImage = sharpImage.resize(options.width, options.height, {
|
||||
// If faces weren't found, default to entropy/attention based cropping as fallback
|
||||
fit: "cover",
|
||||
position: detections.length > 0 ? "center" : "attention",
|
||||
});
|
||||
|
||||
const format = options.format || "webp";
|
||||
const quality = options.quality || 80;
|
||||
|
||||
if (format === "webp") {
|
||||
finalImage = finalImage.webp({ quality });
|
||||
} else if (format === "jpeg") {
|
||||
finalImage = finalImage.jpeg({ quality });
|
||||
} else if (format === "png") {
|
||||
finalImage = finalImage.png({ quality });
|
||||
} else if (format === "avif") {
|
||||
finalImage = finalImage.avif({ quality });
|
||||
}
|
||||
|
||||
return finalImage.toBuffer();
|
||||
}
|
||||
19
packages/image-processor/tsconfig.json
Normal file
19
packages/image-processor/tsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"extends": "@mintel/tsconfig/base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"allowJs": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"**/*.test.ts"
|
||||
]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mintel/infra",
|
||||
"version": "1.8.10",
|
||||
"version": "1.8.12",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://npm.infra.mintel.me"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mintel/journaling",
|
||||
"version": "1.0.0",
|
||||
"version": "1.8.12",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import OpenAI from "openai";
|
||||
import { DataCommonsClient } from "./clients/data-commons";
|
||||
import { TrendsClient } from "./clients/trends";
|
||||
import { SerperClient, type SerperVideoResult } from "./clients/serper";
|
||||
import { SerperClient } from "./clients/serper";
|
||||
|
||||
export interface Fact {
|
||||
statement: string;
|
||||
@@ -54,7 +54,6 @@ export class ResearchAgent {
|
||||
if (data.length > 0) {
|
||||
// Analyze trend
|
||||
const latest = data[data.length - 1];
|
||||
const max = Math.max(...data.map((d) => d.value));
|
||||
facts.push({
|
||||
statement: `Interest in "${kw}" is currently at ${latest.value}% of peak popularity.`,
|
||||
source: "Google Trends",
|
||||
@@ -246,7 +245,7 @@ Return a JSON object with a single string field "query". Example: {"query": "cor
|
||||
const evalPrompt = `You are a strict technical evaluator. You must select the MOST RELEVANT educational tech video from the list below based on this core article context: "${topic.slice(0, 800)}..."
|
||||
|
||||
Videos:
|
||||
${ytVideos.map((v, i) => `[ID: ${i}] Title: "${v.title}" | Channel: "${v.channel}" | Snippet: "${v.snippet || 'none'}"`).join("\n")}
|
||||
${ytVideos.map((v, i) => `[ID: ${i}] Title: "${v.title}" | Channel: "${v.channel}" | Snippet: "${v.snippet || "none"}"`).join("\n")}
|
||||
|
||||
RULES:
|
||||
1. The video MUST be highly relevant to the EXACT technical topic of the context.
|
||||
@@ -268,7 +267,7 @@ Return ONLY a JSON object: {"bestVideoId": number}`;
|
||||
evalResponse.choices[0].message.content || '{"bestVideoId": -1}',
|
||||
);
|
||||
bestIdx = evalParsed.bestVideoId;
|
||||
} catch (e) {
|
||||
} catch {
|
||||
console.warn("Failed to parse video evaluation response");
|
||||
}
|
||||
|
||||
@@ -343,7 +342,7 @@ CRITICAL: Do NOT provide more than 2 trendsKeywords. Keep it extremely focused.`
|
||||
try {
|
||||
let parsed = JSON.parse(
|
||||
response.choices[0].message.content ||
|
||||
'{"trendsKeywords": [], "dcVariables": []}',
|
||||
'{"trendsKeywords": [], "dcVariables": []}',
|
||||
);
|
||||
if (Array.isArray(parsed)) {
|
||||
parsed = parsed[0] || { trendsKeywords: [], dcVariables: [] };
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mintel/mail",
|
||||
"version": "1.8.10",
|
||||
"version": "1.8.12",
|
||||
"private": false,
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mintel/meme-generator",
|
||||
"version": "1.0.0",
|
||||
"version": "1.8.12",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
|
||||
@@ -123,7 +123,7 @@ IMPORTANT: Return ONLY the JSON object. No markdown wrappers.`,
|
||||
let result;
|
||||
try {
|
||||
result = JSON.parse(body);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
console.error("Failed to parse AI response", body);
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mintel/next-config",
|
||||
"version": "1.8.10",
|
||||
"version": "1.8.12",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://npm.infra.mintel.me"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mintel/next-feedback",
|
||||
"version": "1.8.10",
|
||||
"version": "1.8.12",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://npm.infra.mintel.me"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mintel/next-observability",
|
||||
"version": "1.8.10",
|
||||
"version": "1.8.12",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://npm.infra.mintel.me"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mintel/next-utils",
|
||||
"version": "1.8.10",
|
||||
"version": "1.8.12",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://npm.infra.mintel.me"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mintel/observability",
|
||||
"version": "1.8.10",
|
||||
"version": "1.8.12",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://npm.infra.mintel.me"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mintel/pdf",
|
||||
"version": "1.8.10",
|
||||
"version": "1.8.12",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.js",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "people-manager",
|
||||
"description": "Custom High-Fidelity Management for Directus",
|
||||
"icon": "extension",
|
||||
"version": "1.8.10",
|
||||
"version": "1.8.12",
|
||||
"type": "module",
|
||||
"keywords": [
|
||||
"directus",
|
||||
@@ -27,4 +27,4 @@
|
||||
"@directus/extensions-sdk": "11.0.2",
|
||||
"vue": "^3.4.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mintel/thumbnail-generator",
|
||||
"version": "1.0.0",
|
||||
"version": "1.8.12",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mintel/tsconfig",
|
||||
"version": "1.8.10",
|
||||
"version": "1.8.12",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://npm.infra.mintel.me"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "unified-dashboard",
|
||||
"description": "Custom High-Fidelity Management for Directus",
|
||||
"icon": "extension",
|
||||
"version": "1.8.10",
|
||||
"version": "1.8.12",
|
||||
"type": "module",
|
||||
"keywords": [
|
||||
"directus",
|
||||
@@ -24,4 +24,4 @@
|
||||
"@directus/extensions-sdk": "11.0.2",
|
||||
"vue": "^3.4.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1154
pnpm-lock.yaml
generated
1154
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user