Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| efd1341762 | |||
| 36a952db56 | |||
| 8c637f0220 | |||
| 6dd97e7a6b | |||
| 9f426470bb | |||
| 960914ebb8 | |||
| a55a5bb834 | |||
| 0aaf858f5b | |||
| ec562c1b2c | |||
| 02e15c3f4a | |||
| cd4c2193ce | |||
| df7a464e03 | |||
| e2e0653de6 | |||
| 590ae6f69b | |||
| 2a169f1dfc | |||
| 1bbe89c879 | |||
| 554ca81c9b | |||
| aac0fe81b9 | |||
| ada1e9c717 | |||
| 4d295d10d1 | |||
| c00f4e5ea5 |
@@ -24,5 +24,3 @@ coverage
|
||||
**/.pnpm-store
|
||||
.gitea
|
||||
**/.gitea
|
||||
models
|
||||
**/models
|
||||
|
||||
2
.env
2
.env
@@ -1,5 +1,5 @@
|
||||
# Project
|
||||
IMAGE_TAG=v1.8.6
|
||||
IMAGE_TAG=v1.8.19
|
||||
PROJECT_NAME=at-mintel
|
||||
PROJECT_COLOR=#82ed20
|
||||
GITEA_TOKEN=ccce002e30fe16a31a6c9d5a414740af2f72a582
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Project
|
||||
IMAGE_TAG=v1.8.11
|
||||
IMAGE_TAG=v1.8.21
|
||||
PROJECT_NAME=sample-website
|
||||
PROJECT_COLOR=#82ed20
|
||||
|
||||
|
||||
@@ -214,7 +214,7 @@ jobs:
|
||||
with:
|
||||
context: .
|
||||
file: ${{ matrix.file }}
|
||||
platforms: linux/arm64
|
||||
platforms: linux/amd64
|
||||
pull: true
|
||||
provenance: false
|
||||
push: true
|
||||
|
||||
@@ -35,8 +35,9 @@ do
|
||||
# Push the updated tag directly (using --no-verify to avoid recursion)
|
||||
git push origin "$TAG" --force --no-verify
|
||||
|
||||
echo "✨ All done! Hook integrated the sync and pushed for you."
|
||||
exit 1 # Still exit 1 to abort the original (now outdated) push attempt
|
||||
echo "✨ Success! The hook synchronized the versions and pushed the updated tag for you."
|
||||
echo "ℹ️ Note: The original push command was aborted in favor of the auto-push. This is normal."
|
||||
exit 0 # Change to exit 0 to not show as an 'error' in vscode/terminal, though original push will still be technically 'failed' by git
|
||||
else
|
||||
echo "✨ Versions already in sync for $TAG."
|
||||
fi
|
||||
|
||||
@@ -81,3 +81,4 @@ Client websites scaffolded via the CLI use a **tag-based deployment** strategy:
|
||||
|
||||
See the [`@mintel/infra`](packages/infra/README.md) package for detailed template documentation.
|
||||
|
||||
Trigger rebuilding for x86 architecture.
|
||||
|
||||
@@ -1,39 +1,39 @@
|
||||
FROM node:20.18-bookworm-slim AS base
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN apt-get update && apt-get install -y \
|
||||
build-essential \
|
||||
python3 \
|
||||
libcairo2-dev \
|
||||
libpango1.0-dev \
|
||||
libjpeg-dev \
|
||||
libgif-dev \
|
||||
librsvg2-dev \
|
||||
libexpat1 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
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
|
||||
# We only need standard pnpm install now, no C++ tools needed for basic Sharp
|
||||
RUN pnpm install --frozen-lockfile
|
||||
# Force tfjs-node to build the native addon from source so it compiles for arm64 (bypassing pnpm quirks)
|
||||
RUN for f in $(find /app/node_modules/.pnpm -path "*/@tensorflow/tfjs-node/scripts/install.js"); do cd $(dirname $(dirname $f)) && npm run install -- build-addon-from-source; done
|
||||
RUN pnpm install --frozen-lockfile
|
||||
# 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
|
||||
# Instead of copying node_modules which contains native C++ bindings for canvas and tfjs-node,
|
||||
# we copy the package.json files and install natively in the final stage so the bindings are correct.
|
||||
COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./
|
||||
COPY apps/image-service/package.json ./apps/image-service/package.json
|
||||
COPY packages/image-processor/package.json ./packages/image-processor/package.json
|
||||
|
||||
# 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/*
|
||||
RUN pnpm install --frozen-lockfile --filter image-service...
|
||||
|
||||
COPY --from=build /app/apps/image-service/dist ./apps/image-service/dist
|
||||
COPY --from=build /app/packages/image-processor/dist ./packages/image-processor/dist
|
||||
COPY --from=build /app/packages/image-processor/models ./packages/image-processor/models
|
||||
|
||||
EXPOSE 8080
|
||||
WORKDIR /app/apps/image-service
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "image-service",
|
||||
"version": "1.8.11",
|
||||
"version": "1.8.21",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,45 +1,51 @@
|
||||
import Fastify from "fastify";
|
||||
import { processImageWithSmartCrop } from "@mintel/image-processor";
|
||||
import {
|
||||
processImageWithSmartCrop,
|
||||
parseImgproxyOptions,
|
||||
mapUrl,
|
||||
} 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 { options, urlSafeB64 } = request.params as {
|
||||
options: string;
|
||||
urlSafeB64: 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' });
|
||||
// urlSafeB64 might be "plain/http://..." or a Base64 string
|
||||
let url = "";
|
||||
if (urlSafeB64.startsWith("plain/")) {
|
||||
url = urlSafeB64.substring(6);
|
||||
} else {
|
||||
try {
|
||||
url = Buffer.from(urlSafeB64, "base64").toString("utf-8");
|
||||
} catch (e) {
|
||||
return reply.status(400).send({ error: "Invalid Base64 URL" });
|
||||
}
|
||||
}
|
||||
|
||||
const parsedOptions = parseImgproxyOptions(options);
|
||||
const mappedUrl = mapUrl(url, process.env.IMGPROXY_URL_MAPPING);
|
||||
|
||||
return handleProcessing(mappedUrl, parsedOptions, reply);
|
||||
});
|
||||
|
||||
// Helper to avoid duplication
|
||||
async function handleProcessing(url: string, options: any, reply: any) {
|
||||
const width = options.width || 800;
|
||||
const height = options.height || 600;
|
||||
const quality = options.quality || 80;
|
||||
const format = options.format || "webp";
|
||||
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
return reply
|
||||
.status(response.status)
|
||||
.send({
|
||||
error: `Failed to fetch source image: ${response.statusText}`,
|
||||
});
|
||||
return reply.status(response.status).send({
|
||||
error: `Failed to fetch source image: ${response.statusText}`,
|
||||
});
|
||||
}
|
||||
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
@@ -61,6 +67,29 @@ fastify.get("/process", async (request, reply) => {
|
||||
.status(500)
|
||||
.send({ error: "Internal Server Error processing image" });
|
||||
}
|
||||
}
|
||||
|
||||
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 any;
|
||||
|
||||
if (!url) {
|
||||
return reply.status(400).send({ error: 'Parameter "url" is required' });
|
||||
}
|
||||
|
||||
const mappedUrl = mapUrl(url, process.env.IMGPROXY_URL_MAPPING);
|
||||
return handleProcessing(mappedUrl, { width, height, quality, format }, reply);
|
||||
});
|
||||
|
||||
fastify.get("/health", async () => {
|
||||
|
||||
@@ -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.11",
|
||||
"version": "1.8.21",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,45 +1,60 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { processImageWithSmartCrop } from '@mintel/image-processor';
|
||||
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');
|
||||
let width = parseInt(searchParams.get('w') || '800');
|
||||
let height = parseInt(searchParams.get('h') || '600');
|
||||
let q = parseInt(searchParams.get('q') || '80');
|
||||
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 });
|
||||
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 },
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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,
|
||||
});
|
||||
// 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 });
|
||||
}
|
||||
// 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 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
14
optimize-images.sh
Normal file
14
optimize-images.sh
Normal file
@@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Ghost Image Optimizer
|
||||
# Target directory for Ghost content
|
||||
TARGET_DIR="/home/deploy/sites/marisas.world/content/images"
|
||||
|
||||
echo "Starting image optimization for $TARGET_DIR..."
|
||||
|
||||
# Find all original images, excluding the 'size/' directory where Ghost stores thumbnails
|
||||
# Resize images larger than 2500px down to 2500px width
|
||||
# Compress JPEG/PNG to 80% quality
|
||||
find "$TARGET_DIR" -type d -name "size" -prune -o \( -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.png" \) -type f -exec mogrify -resize '2500x>' -quality 80 {} +
|
||||
|
||||
echo "Optimization complete."
|
||||
@@ -57,7 +57,7 @@
|
||||
"pino-pretty": "^13.1.3",
|
||||
"require-in-the-middle": "^8.0.1"
|
||||
},
|
||||
"version": "1.8.11",
|
||||
"version": "1.8.21",
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"@parcel/watcher",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "acquisition-manager",
|
||||
"description": "Custom High-Fidelity Management for Directus",
|
||||
"icon": "extension",
|
||||
"version": "1.8.11",
|
||||
"version": "1.8.21",
|
||||
"type": "module",
|
||||
"keywords": [
|
||||
"directus",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "acquisition",
|
||||
"version": "1.8.11",
|
||||
"version": "1.8.21",
|
||||
"type": "module",
|
||||
"directus:extension": {
|
||||
"type": "endpoint",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mintel/cli",
|
||||
"version": "1.8.11",
|
||||
"version": "1.8.21",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://npm.infra.mintel.me"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mintel/cloner",
|
||||
"version": "1.8.11",
|
||||
"version": "1.8.21",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.js",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mintel/cms-infra",
|
||||
"version": "1.8.11",
|
||||
"version": "1.8.21",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "company-manager",
|
||||
"description": "Custom High-Fidelity Management for Directus",
|
||||
"icon": "extension",
|
||||
"version": "1.8.11",
|
||||
"version": "1.8.21",
|
||||
"type": "module",
|
||||
"keywords": [
|
||||
"directus",
|
||||
|
||||
38
packages/concept-engine/package.json
Normal file
38
packages/concept-engine/package.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "@mintel/concept-engine",
|
||||
"version": "1.8.21",
|
||||
"private": true,
|
||||
"description": "AI-powered web project concept generation and analysis",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"bin": {
|
||||
"concept": "./dist/cli.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"dev": "tsup --watch",
|
||||
"test": "vitest",
|
||||
"clean": "rm -rf dist",
|
||||
"lint": "eslint src --ext .ts",
|
||||
"concept": "tsx src/cli.ts run"
|
||||
},
|
||||
"dependencies": {
|
||||
"@crawlee/cheerio": "^3.11.2",
|
||||
"@mintel/journaling": "workspace:*",
|
||||
"@mintel/page-audit": "workspace:*",
|
||||
"axios": "^1.7.9",
|
||||
"cheerio": "1.0.0-rc.12",
|
||||
"commander": "^13.1.0",
|
||||
"dotenv": "^16.4.7",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.17.17",
|
||||
"tsup": "^8.3.6",
|
||||
"tsx": "^4.19.2",
|
||||
"typescript": "^5.7.3",
|
||||
"vitest": "^3.0.5"
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@mintel/content-engine",
|
||||
"version": "1.8.11",
|
||||
"private": true,
|
||||
"version": "1.8.21",
|
||||
"private": false,
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
|
||||
@@ -17,6 +17,7 @@ export interface OptimizationTask {
|
||||
availableComponents?: ComponentDefinition[];
|
||||
instructions?: string;
|
||||
internalLinks?: { title: string; slug: string }[];
|
||||
customSources?: string[];
|
||||
}
|
||||
|
||||
export interface OptimizeFileOptions {
|
||||
@@ -211,7 +212,32 @@ export class AiBlogPostOrchestrator {
|
||||
console.log(`✅ Saved optimized file to: ${finalPath}`);
|
||||
}
|
||||
|
||||
private async generateVisualPrompt(content: string): Promise<string> {
|
||||
async generateSlug(content: string, title?: string, instructions?: string): Promise<string> {
|
||||
const response = await this.openai.chat.completions.create({
|
||||
model: "google/gemini-2.5-flash",
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content: `You generate SEO-optimized URL slugs for B2B blog posts based on the provided content.
|
||||
Return ONLY a JSON object with a single string field "slug".
|
||||
Example: {"slug": "how-to-optimize-react-performance"}
|
||||
Rules: Use lowercase letters, numbers, and hyphens only. No special characters. Keep it concise (2-5 words).`,
|
||||
},
|
||||
{ role: "user", content: `Title: ${title || "Unknown"}\n\nContent:\n${content.slice(0, 3000)}...${instructions ? `\n\nEDITOR INSTRUCTIONS:\nPlease strictly follow these instructions from the editor when generating the slug:\n${instructions}` : ""}` },
|
||||
],
|
||||
response_format: { type: "json_object" },
|
||||
});
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(response.choices[0].message.content || '{"slug": ""}');
|
||||
let slug = parsed.slug || "new-post";
|
||||
return slug.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
||||
} catch {
|
||||
return "new-post";
|
||||
}
|
||||
}
|
||||
|
||||
public async generateVisualPrompt(content: string, instructions?: string): Promise<string> {
|
||||
const response = await this.openai.chat.completions.create({
|
||||
model: this.model,
|
||||
messages: [
|
||||
@@ -227,7 +253,7 @@ FOCUS: The core metaphor or technical concept of the article.
|
||||
|
||||
Example output: "A complex network of glowing fiber optic nodes forming a recursive pyramid structure, technical blue lineart style."`,
|
||||
},
|
||||
{ role: "user", content: content.slice(0, 5000) },
|
||||
{ role: "user", content: `${content.slice(0, 5000)}${instructions ? `\n\nEDITOR INSTRUCTIONS:\nPlease strictly follow these instructions from the editor when generating the visual prompt:\n${instructions}` : ""}` },
|
||||
],
|
||||
max_tokens: 100,
|
||||
});
|
||||
@@ -303,6 +329,7 @@ Example output: "A complex network of glowing fiber optic nodes forming a recurs
|
||||
);
|
||||
const realPosts = await this.researchAgent.fetchRealSocialPosts(
|
||||
task.content.slice(0, 500),
|
||||
task.customSources
|
||||
);
|
||||
socialPosts.push(...realPosts);
|
||||
}
|
||||
@@ -470,7 +497,6 @@ BLOG POST BEST PRACTICES (MANDATORY):
|
||||
- MEME DIVERSITY: Du MUSST ZWINGEND für jedes Meme (sofern passend) abwechslungsreiche Templates nutzen. Um dies zu garantieren, wurde für diesen Artikel das folgende Template ausgewählt: '${forcedMeme}'. Du MUSST EXAKT DIESES TEMPLATE NUTZEN. Versuche nicht, es durch ein Standard-Template wie 'drake' zu ersetzen!
|
||||
- Zitat-Varianten: Wenn du Organisationen oder Studien zitierst, nutze ArticleQuote (mit isCompany=true für Firmen). Für Personen lass isCompany weg.
|
||||
- Füge zwingend ein prägnantes 'TL;DR' ganz am Anfang ein.
|
||||
- Füge ein sauberes TableOfContents ein.
|
||||
- Verwende unsere Komponenten stilvoll für Visualisierungen.
|
||||
- Agiere als hochprofessioneller Digital Architect und entferne alte MDX-Metadaten im Body.
|
||||
- Fazit: Schließe JEDEN Artikel ZWINGEND mit einem starken, klaren 'Fazit' ab.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "customer-manager",
|
||||
"description": "Custom High-Fidelity Management for Directus",
|
||||
"icon": "extension",
|
||||
"version": "1.8.11",
|
||||
"version": "1.8.21",
|
||||
"type": "module",
|
||||
"keywords": [
|
||||
"directus",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mintel/directus-extension-toolkit",
|
||||
"version": "1.8.11",
|
||||
"version": "1.8.21",
|
||||
"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.11",
|
||||
"version": "1.8.21",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://npm.infra.mintel.me"
|
||||
|
||||
46
packages/estimation-engine/package.json
Normal file
46
packages/estimation-engine/package.json
Normal file
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "@mintel/estimation-engine",
|
||||
"version": "1.8.21",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"bin": {
|
||||
"estimate": "./dist/cli.js"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsup src/index.ts src/cli.ts --format esm --dts --clean",
|
||||
"dev": "tsup src/index.ts src/cli.ts --format esm --watch --dts",
|
||||
"lint": "eslint src",
|
||||
"estimate": "tsx src/cli.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mintel/concept-engine": "workspace:*",
|
||||
"axios": "^1.6.0",
|
||||
"chalk": "^5.3.0",
|
||||
"commander": "^12.0.0",
|
||||
"dotenv": "^17.3.1",
|
||||
"ink": "^5.1.0",
|
||||
"ink-spinner": "^5.0.0",
|
||||
"ink-select-input": "^6.0.0",
|
||||
"ink-text-input": "^6.0.0",
|
||||
"react": "^18.2.0",
|
||||
"openai": "^4.82.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@mintel/eslint-config": "workspace:*",
|
||||
"@mintel/tsconfig": "workspace:*",
|
||||
"@types/node": "^20.0.0",
|
||||
"@types/react": "^18.2.0",
|
||||
"tsup": "^8.3.5",
|
||||
"tsx": "^4.7.0",
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "feedback-commander",
|
||||
"description": "Custom High-Fidelity Management for Directus",
|
||||
"icon": "extension",
|
||||
"version": "1.8.11",
|
||||
"version": "1.8.21",
|
||||
"type": "module",
|
||||
"keywords": [
|
||||
"directus",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mintel/gatekeeper",
|
||||
"version": "1.8.11",
|
||||
"version": "1.8.21",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mintel/husky-config",
|
||||
"version": "1.8.11",
|
||||
"version": "1.8.21",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://npm.infra.mintel.me"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
404: Not Found
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mintel/image-processor",
|
||||
"version": "1.8.11",
|
||||
"version": "1.8.21",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
@@ -13,14 +13,14 @@
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsup src/index.ts --format esm --dts --clean",
|
||||
"dev": "tsup src/index.ts --format esm --watch --dts",
|
||||
"build": "tsup",
|
||||
"dev": "tsup --watch",
|
||||
"lint": "eslint src"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tensorflow/tfjs-node": "^4.22.0",
|
||||
"@vladmandic/face-api": "^1.7.13",
|
||||
"canvas": "^2.11.2",
|
||||
"@tensorflow/tfjs": "^4.22.0",
|
||||
"@vladmandic/face-api": "^1.7.15",
|
||||
"canvas": "^3.2.1",
|
||||
"sharp": "^0.33.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
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,25 +1,33 @@
|
||||
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";
|
||||
import { Canvas, Image, ImageData } from "canvas";
|
||||
// Use the ESM no-bundle build to avoid the default Node entrypoint
|
||||
// which hardcodes require('@tensorflow/tfjs-node') and crashes in Docker.
|
||||
// This build uses pure @tensorflow/tfjs (JS-only, no native C++ bindings).
|
||||
// @ts-ignore - direct path import has no type declarations
|
||||
import * as faceapi from "@vladmandic/face-api/dist/face-api.esm-nobundle.js";
|
||||
import * as tf from "@tensorflow/tfjs";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
// @ts-expect-error FaceAPI does not have type definitions for monkeyPatch
|
||||
faceapi.env.monkeyPatch({ Canvas, Image, ImageData });
|
||||
// Polyfill required by face-api for Node.js
|
||||
faceapi.env.monkeyPatch({ Canvas, Image, ImageData } as any);
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const MODEL_URL = path.join(__dirname, "../models");
|
||||
|
||||
// Path to the downloaded models
|
||||
const MODELS_PATH = path.join(__dirname, "..", "models");
|
||||
// State flag to ensure we only load weights once
|
||||
let modelsLoaded = false;
|
||||
|
||||
let isModelsLoaded = false;
|
||||
async function loadModelsOnce() {
|
||||
if (modelsLoaded) return;
|
||||
// Initialize pure JS CPU backend (no native bindings needed)
|
||||
await tf.setBackend("cpu");
|
||||
await tf.ready();
|
||||
|
||||
async function loadModels() {
|
||||
if (isModelsLoaded) return;
|
||||
await faceapi.nets.tinyFaceDetector.loadFromDisk(MODELS_PATH);
|
||||
isModelsLoaded = true;
|
||||
// Load the microscopic TinyFaceDetector (~190KB)
|
||||
await faceapi.nets.tinyFaceDetector.loadFromDisk(MODEL_URL);
|
||||
modelsLoaded = true;
|
||||
}
|
||||
|
||||
export interface ProcessImageOptions {
|
||||
@@ -29,23 +37,82 @@ export interface ProcessImageOptions {
|
||||
quality?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a URL based on the IMGPROXY_URL_MAPPING environment variable.
|
||||
* Format: "match1:replace1,match2:replace2"
|
||||
*/
|
||||
export function mapUrl(url: string, mappingString?: string): string {
|
||||
if (!mappingString) return url;
|
||||
|
||||
const mappings = mappingString.split(",").map((m) => {
|
||||
if (m.includes("|")) {
|
||||
return m.split("|");
|
||||
}
|
||||
|
||||
// Legacy support for simple "host:target" or cases where one side might have a protocol
|
||||
// We try to find the split point that isn't part of a protocol "://"
|
||||
const colonIndices = [];
|
||||
for (let i = 0; i < m.length; i++) {
|
||||
if (m[i] === ":") {
|
||||
// Check if this colon is part of "://"
|
||||
if (!(m[i + 1] === "/" && m[i + 2] === "/")) {
|
||||
colonIndices.push(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (colonIndices.length === 0) return [m];
|
||||
|
||||
// In legacy mode with colons, we take the LAST non-protocol colon as the separator
|
||||
// This handles "http://host:port" or "host:http://target" better
|
||||
const lastColon = colonIndices[colonIndices.length - 1];
|
||||
return [m.substring(0, lastColon), m.substring(lastColon + 1)];
|
||||
});
|
||||
|
||||
let mappedUrl = url;
|
||||
|
||||
for (const [match, replace] of mappings) {
|
||||
if (match && replace && url.includes(match)) {
|
||||
mappedUrl = url.replace(match, replace);
|
||||
}
|
||||
}
|
||||
|
||||
return mappedUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses legacy imgproxy options string.
|
||||
* Example: rs:fill:300:400/q:80
|
||||
*/
|
||||
export function parseImgproxyOptions(
|
||||
optionsStr: string,
|
||||
): Partial<ProcessImageOptions> {
|
||||
const parts = optionsStr.split("/");
|
||||
const options: Partial<ProcessImageOptions> = {};
|
||||
|
||||
for (const part of parts) {
|
||||
if (part.startsWith("rs:")) {
|
||||
const [, , w, h] = part.split(":");
|
||||
if (w) options.width = parseInt(w, 10);
|
||||
if (h) options.height = parseInt(h, 10);
|
||||
} else if (part.startsWith("q:")) {
|
||||
const q = part.split(":")[1];
|
||||
if (q) options.quality = parseInt(q, 10);
|
||||
} else if (part.startsWith("ext:")) {
|
||||
const ext = part.split(":")[1] as any;
|
||||
if (["webp", "jpeg", "png", "avif"].includes(ext)) {
|
||||
options.format = ext;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
@@ -53,35 +120,45 @@ export async function processImageWithSmartCrop(
|
||||
throw new Error("Could not read image metadata");
|
||||
}
|
||||
|
||||
// If faces are found, calculate the bounding box containing all faces
|
||||
// Load ML models (noop if already loaded)
|
||||
await loadModelsOnce();
|
||||
|
||||
// Convert sharp image to a Node-compatible canvas Image for face-api
|
||||
const jpegBuffer = await sharpImage.jpeg().toBuffer();
|
||||
const img = new Image();
|
||||
img.src = jpegBuffer;
|
||||
const canvas = new Canvas(img.width, img.height);
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.drawImage(img, 0, 0, img.width, img.height);
|
||||
|
||||
// Detect faces locally using the tiny model
|
||||
// Requires explicit any cast since the types expect HTML elements in browser contexts
|
||||
const detections = await faceapi.detectAllFaces(
|
||||
canvas as any,
|
||||
new faceapi.TinyFaceDetectorOptions(),
|
||||
);
|
||||
|
||||
let cropPosition: "center" | "attention" | number = "attention"; // Fallback to sharp's attention if no faces
|
||||
|
||||
if (detections.length > 0) {
|
||||
// We have faces! Calculate the bounding box that contains all of them
|
||||
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 box = det.box;
|
||||
if (box.x < minX) minX = Math.max(0, box.x);
|
||||
if (box.y < minY) minY = Math.max(0, box.y);
|
||||
if (box.x + box.width > maxX)
|
||||
maxX = Math.min(metadata.width, box.x + box.width);
|
||||
if (box.y + box.height > maxY)
|
||||
maxY = Math.min(metadata.height, box.y + box.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 centerX = Math.floor(minX + (maxX - minX) / 2);
|
||||
const centerY = Math.floor(minY + (maxY - minY) / 2);
|
||||
|
||||
const targetRatio = options.width / options.height;
|
||||
const currentRatio = metadata.width / metadata.height;
|
||||
@@ -89,38 +166,39 @@ export async function processImageWithSmartCrop(
|
||||
let cropWidth = metadata.width;
|
||||
let cropHeight = metadata.height;
|
||||
|
||||
// Determine the maximal crop window that maintains aspect ratio
|
||||
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
|
||||
// Center the crop window over the center of the faces
|
||||
let cropX = Math.floor(centerX - cropWidth / 2);
|
||||
let cropY = Math.floor(centerY - cropHeight / 2);
|
||||
|
||||
// Keep crop box within image bounds
|
||||
// Keep crop window inside 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;
|
||||
|
||||
// Pre-crop the image to isolate the faces before resizing
|
||||
sharpImage.extract({
|
||||
left: cropX,
|
||||
top: cropY,
|
||||
width: cropWidth,
|
||||
height: cropHeight,
|
||||
});
|
||||
|
||||
// As we manually calculated the exact focal box, we can now just center it
|
||||
cropPosition = "center";
|
||||
}
|
||||
|
||||
// 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",
|
||||
position: cropPosition,
|
||||
});
|
||||
|
||||
const format = options.format || "webp";
|
||||
|
||||
19
packages/image-processor/tsup.config.ts
Normal file
19
packages/image-processor/tsup.config.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { defineConfig } from "tsup";
|
||||
|
||||
export default defineConfig({
|
||||
entry: ["src/index.ts"],
|
||||
format: ["esm"],
|
||||
dts: true,
|
||||
clean: true,
|
||||
// Bundle face-api and tensorflow inline (they're pure JS).
|
||||
// Keep sharp and canvas external (they have native C++ bindings).
|
||||
noExternal: [
|
||||
"@vladmandic/face-api",
|
||||
"@tensorflow/tfjs",
|
||||
"@tensorflow/tfjs-backend-wasm"
|
||||
],
|
||||
external: [
|
||||
"sharp",
|
||||
"canvas"
|
||||
],
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
# Step 1: Builder stage
|
||||
FROM node:20-alpine AS builder
|
||||
RUN apk add --no-cache libc6-compat curl
|
||||
RUN apk add --no-cache libc6-compat curl python3 make g++ pkgconfig pixman-dev cairo-dev pango-dev
|
||||
WORKDIR /app
|
||||
RUN corepack enable pnpm
|
||||
ENV CI=true
|
||||
@@ -25,7 +25,7 @@ RUN --mount=type=cache,id=pnpm,target=/pnpm/store \
|
||||
--mount=type=secret,id=NPM_TOKEN \
|
||||
export NPM_TOKEN=$(cat /run/secrets/NPM_TOKEN) && \
|
||||
pnpm config set store-dir /pnpm/store && \
|
||||
pnpm i --frozen-lockfile
|
||||
pnpm i --no-frozen-lockfile
|
||||
|
||||
# Copy the rest of the source
|
||||
COPY . .
|
||||
|
||||
@@ -189,7 +189,7 @@ jobs:
|
||||
with:
|
||||
context: .
|
||||
file: packages/infra/docker/Dockerfile.nextjs
|
||||
platforms: linux/arm64
|
||||
platforms: linux/amd64
|
||||
pull: true
|
||||
provenance: false
|
||||
build-args: |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mintel/infra",
|
||||
"version": "1.8.11",
|
||||
"version": "1.8.21",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://npm.infra.mintel.me"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mintel/journaling",
|
||||
"version": "1.8.11",
|
||||
"version": "1.8.21",
|
||||
"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",
|
||||
@@ -177,6 +176,7 @@ Return JSON: { "facts": [ { "statement": "...", "source": "Organization Name Onl
|
||||
*/
|
||||
async fetchRealSocialPosts(
|
||||
topic: string,
|
||||
customSources?: string[],
|
||||
retries = 1,
|
||||
): Promise<SocialPost[]> {
|
||||
console.log(
|
||||
@@ -221,7 +221,7 @@ Return a JSON object with a single string field "query". Example: {"query": "cor
|
||||
|
||||
if (!videos || videos.length === 0) {
|
||||
console.warn(`⚠️ [Serper] No videos found for query: "${queryStr}"`);
|
||||
if (retries > 0) return this.fetchRealSocialPosts(topic, retries - 1);
|
||||
if (retries > 0) return this.fetchRealSocialPosts(topic, customSources, retries - 1);
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -238,19 +238,24 @@ Return a JSON object with a single string field "query". Example: {"query": "cor
|
||||
|
||||
if (ytVideos.length === 0) {
|
||||
console.warn(`⚠️ [Serper] No YouTube videos in search results.`);
|
||||
if (retries > 0) return this.fetchRealSocialPosts(topic, retries - 1);
|
||||
if (retries > 0) return this.fetchRealSocialPosts(topic, customSources, retries - 1);
|
||||
return [];
|
||||
}
|
||||
|
||||
// Step 3: Ask the LLM to evaluate the relevance of the found videos
|
||||
|
||||
const sourceExamples = customSources && customSources.length > 0
|
||||
? `Specifically prioritize content from: ${customSources.join(", ")}.`
|
||||
: `(e.g., Google Developers, Vercel, Theo - t3.gg, Fireship, Syntax, ByteByteGo, IBM Technology, McKinsey, Gartner, Deloitte).`;
|
||||
|
||||
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.
|
||||
2. The channel SHOULD be a high-quality tech, development, or professional B2B channel (e.g., Google Developers, Vercel, Theo - t3.gg, Fireship, Syntax, ByteByteGo, IBM Technology, McKinsey, Gartner, Deloitte). AVOID gaming, generic vlogs, clickbait, off-topic podcasts, or unrelated topics.
|
||||
2. The channel SHOULD be a high-quality tech, development, or professional B2B channel ${sourceExamples} AVOID gaming, generic vlogs, clickbait, off-topic podcasts, or unrelated topics.
|
||||
3. If none of the videos are strictly relevant to the core technical or business subject (e.g. they are just casually mentioning the word), YOU MUST RETURN -1. Be extremely critical. Do not just pick the "best of the worst".
|
||||
4. If one is highly relevant, return its ID number.
|
||||
|
||||
@@ -268,13 +273,13 @@ 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");
|
||||
}
|
||||
|
||||
if (bestIdx < 0 || bestIdx >= ytVideos.length) {
|
||||
console.warn(`⚠️ [Serper] LLM rejected all videos as irrelevant.`);
|
||||
if (retries > 0) return this.fetchRealSocialPosts(topic, retries - 1);
|
||||
if (retries > 0) return this.fetchRealSocialPosts(topic, customSources, retries - 1);
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mintel/mail",
|
||||
"version": "1.8.11",
|
||||
"version": "1.8.21",
|
||||
"private": false,
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@mintel/meme-generator",
|
||||
"version": "1.8.11",
|
||||
"private": true,
|
||||
"version": "1.8.21",
|
||||
"private": false,
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./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.11",
|
||||
"version": "1.8.21",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://npm.infra.mintel.me"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mintel/next-feedback",
|
||||
"version": "1.8.11",
|
||||
"version": "1.8.21",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://npm.infra.mintel.me"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mintel/next-observability",
|
||||
"version": "1.8.11",
|
||||
"version": "1.8.21",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://npm.infra.mintel.me"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mintel/next-utils",
|
||||
"version": "1.8.11",
|
||||
"version": "1.8.21",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://npm.infra.mintel.me"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mintel/observability",
|
||||
"version": "1.8.11",
|
||||
"version": "1.8.21",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://npm.infra.mintel.me"
|
||||
|
||||
39
packages/page-audit/package.json
Normal file
39
packages/page-audit/package.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "@mintel/page-audit",
|
||||
"version": "1.8.21",
|
||||
"private": true,
|
||||
"description": "AI-powered website IST-analysis using DataForSEO and Gemini",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"bin": {
|
||||
"page-audit": "./dist/cli.js"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsup src/index.ts src/cli.ts --format esm --dts --clean",
|
||||
"dev": "tsup src/index.ts src/cli.ts --format esm --watch --dts",
|
||||
"audit": "tsx src/cli.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"chalk": "^5.3.0",
|
||||
"cheerio": "^1.0.0",
|
||||
"commander": "^12.0.0",
|
||||
"dotenv": "^17.3.1",
|
||||
"openai": "^4.82.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@mintel/eslint-config": "workspace:*",
|
||||
"@mintel/tsconfig": "workspace:*",
|
||||
"@types/node": "^20.0.0",
|
||||
"tsup": "^8.3.5",
|
||||
"tsx": "^4.7.0",
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mintel/pdf",
|
||||
"version": "1.8.11",
|
||||
"version": "1.8.21",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.js",
|
||||
|
||||
@@ -13,6 +13,8 @@ import {
|
||||
Footer,
|
||||
FoldingMarks,
|
||||
DocumentTitle,
|
||||
COLORS,
|
||||
FONT_SIZES,
|
||||
} from "./pdf/SharedUI.js";
|
||||
import { SimpleLayout } from "./pdf/SimpleLayout.js";
|
||||
|
||||
@@ -29,23 +31,23 @@ const localStyles = PDFStyleSheet.create({
|
||||
marginBottom: 6,
|
||||
},
|
||||
monoNumber: {
|
||||
fontSize: 7,
|
||||
fontSize: FONT_SIZES.TINY,
|
||||
fontWeight: "bold",
|
||||
color: "#94a3b8",
|
||||
color: COLORS.TEXT_LIGHT,
|
||||
letterSpacing: 2,
|
||||
width: 25,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 9,
|
||||
fontSize: FONT_SIZES.LABEL,
|
||||
fontWeight: "bold",
|
||||
color: "#000000",
|
||||
color: COLORS.CHARCOAL,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: 0.5,
|
||||
},
|
||||
officialText: {
|
||||
fontSize: 8,
|
||||
fontSize: FONT_SIZES.BODY,
|
||||
lineHeight: 1.5,
|
||||
color: "#334155",
|
||||
color: COLORS.TEXT_MAIN,
|
||||
textAlign: "justify",
|
||||
paddingLeft: 25,
|
||||
},
|
||||
@@ -100,7 +102,7 @@ export const AgbsPDF = ({
|
||||
};
|
||||
|
||||
const content = (
|
||||
<>
|
||||
<PDFView>
|
||||
<DocumentTitle
|
||||
title="Allgemeine Geschäftsbedingungen"
|
||||
subLines={[`Stand: ${date}`]}
|
||||
@@ -142,7 +144,7 @@ export const AgbsPDF = ({
|
||||
|
||||
<AGBSection index="05" title="Abnahme">
|
||||
Die Leistung gilt als abgenommen, wenn der Auftraggeber sie produktiv
|
||||
nutzt oder innerhalb von 7 Tagen nach Bereitstellung keine
|
||||
nutzt oder innerhalb von 30 Tagen nach Bereitstellung keine
|
||||
wesentlichen Mängel angezeigt werden. Optische Abweichungen,
|
||||
Geschmacksfragen oder subjektive Einschätzungen stellen keine Mängel
|
||||
dar.
|
||||
@@ -206,7 +208,7 @@ export const AgbsPDF = ({
|
||||
bleibt die Wirksamkeit der übrigen Regelungen unberührt.
|
||||
</AGBSection>
|
||||
</PDFView>
|
||||
</>
|
||||
</PDFView>
|
||||
);
|
||||
|
||||
if (mode === "full") {
|
||||
@@ -214,9 +216,8 @@ export const AgbsPDF = ({
|
||||
<SimpleLayout
|
||||
companyData={companyData}
|
||||
bankData={bankData}
|
||||
headerIcon={headerIcon}
|
||||
footerLogo={footerLogo}
|
||||
icon={headerIcon}
|
||||
pageNumber="10"
|
||||
showPageNumber={false}
|
||||
>
|
||||
{content}
|
||||
@@ -232,7 +233,7 @@ export const AgbsPDF = ({
|
||||
<Footer
|
||||
logo={footerLogo}
|
||||
companyData={companyData}
|
||||
bankData={bankData}
|
||||
_bankData={bankData}
|
||||
showDetails={false}
|
||||
showPageNumber={false}
|
||||
/>
|
||||
|
||||
@@ -46,7 +46,7 @@ export const CombinedQuotePDF = ({
|
||||
|
||||
const layoutProps = {
|
||||
date,
|
||||
icon: estimationProps.headerIcon,
|
||||
headerIcon: estimationProps.headerIcon,
|
||||
footerLogo: estimationProps.footerLogo,
|
||||
companyData,
|
||||
bankData,
|
||||
@@ -71,7 +71,7 @@ export const CombinedQuotePDF = ({
|
||||
footerLogo={estimationProps.footerLogo}
|
||||
/>
|
||||
)}
|
||||
<SimpleLayout {...layoutProps} pageNumber="END" showPageNumber={false}>
|
||||
<SimpleLayout {...layoutProps} showPageNumber={false}>
|
||||
<ClosingModule />
|
||||
</SimpleLayout>
|
||||
</PDFDocument>
|
||||
|
||||
@@ -50,7 +50,7 @@ export const EstimationPDF = ({
|
||||
const commonProps = {
|
||||
state,
|
||||
date,
|
||||
icon: headerIcon,
|
||||
headerIcon,
|
||||
footerLogo,
|
||||
companyData,
|
||||
};
|
||||
@@ -64,17 +64,17 @@ export const EstimationPDF = ({
|
||||
<FrontPageModule state={state} headerIcon={headerIcon} date={date} />
|
||||
</PDFPage>
|
||||
|
||||
<SimpleLayout {...commonProps} pageNumber={getPageNum()}>
|
||||
<SimpleLayout {...commonProps}>
|
||||
<BriefingModule state={state} />
|
||||
</SimpleLayout>
|
||||
|
||||
{state.sitemap && state.sitemap.length > 0 && (
|
||||
<SimpleLayout {...commonProps} pageNumber={getPageNum()}>
|
||||
<SimpleLayout {...commonProps}>
|
||||
<SitemapModule state={state} />
|
||||
</SimpleLayout>
|
||||
)}
|
||||
|
||||
<SimpleLayout {...commonProps} pageNumber={getPageNum()}>
|
||||
<SimpleLayout {...commonProps}>
|
||||
<EstimationModule
|
||||
state={state}
|
||||
positions={positions}
|
||||
@@ -83,11 +83,11 @@ export const EstimationPDF = ({
|
||||
/>
|
||||
</SimpleLayout>
|
||||
|
||||
<SimpleLayout {...commonProps} pageNumber={getPageNum()}>
|
||||
<SimpleLayout {...commonProps}>
|
||||
<TransparenzModule pricing={pricing} />
|
||||
</SimpleLayout>
|
||||
|
||||
<SimpleLayout {...commonProps} pageNumber={getPageNum()}>
|
||||
<SimpleLayout {...commonProps}>
|
||||
<ClosingModule />
|
||||
</SimpleLayout>
|
||||
</PDFDocument>
|
||||
|
||||
@@ -8,58 +8,48 @@ const simpleStyles = StyleSheet.create({
|
||||
industrialPage: {
|
||||
padding: 30,
|
||||
paddingTop: 20,
|
||||
backgroundColor: '#ffffff',
|
||||
flexDirection: 'column',
|
||||
backgroundColor: '#FFFFFF',
|
||||
fontFamily: 'Outfit',
|
||||
},
|
||||
industrialNumber: {
|
||||
fontSize: 60,
|
||||
fontWeight: 'bold',
|
||||
color: '#f1f5f9',
|
||||
position: 'absolute',
|
||||
top: -10,
|
||||
right: 0,
|
||||
zIndex: -1,
|
||||
},
|
||||
industrialSection: {
|
||||
marginTop: 16,
|
||||
paddingTop: 12,
|
||||
flexDirection: 'row',
|
||||
position: 'relative',
|
||||
contentView: {
|
||||
flex: 1,
|
||||
marginTop: 20,
|
||||
},
|
||||
});
|
||||
|
||||
interface SimpleLayoutProps {
|
||||
children: React.ReactNode;
|
||||
pageNumber?: string;
|
||||
icon?: string;
|
||||
headerIcon?: string;
|
||||
footerLogo?: string;
|
||||
companyData: any;
|
||||
bankData?: any;
|
||||
showDetails?: boolean;
|
||||
showPageNumber?: boolean;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const SimpleLayout = ({
|
||||
children,
|
||||
pageNumber,
|
||||
icon,
|
||||
export const SimpleLayout: React.FC<SimpleLayoutProps> = ({
|
||||
headerIcon,
|
||||
footerLogo,
|
||||
companyData,
|
||||
bankData,
|
||||
showPageNumber = true
|
||||
}: SimpleLayoutProps) => {
|
||||
showDetails = false,
|
||||
showPageNumber = true,
|
||||
children,
|
||||
}) => {
|
||||
return (
|
||||
<PDFPage size="A4" style={[pdfStyles.page, simpleStyles.industrialPage]}>
|
||||
<Header icon={icon} showAddress={false} />
|
||||
{pageNumber && <PDFText style={simpleStyles.industrialNumber}>{pageNumber}</PDFText>}
|
||||
<PDFView style={simpleStyles.industrialSection}>
|
||||
<PDFView style={{ width: '100%' }}>
|
||||
{children}
|
||||
</PDFView>
|
||||
<PDFPage size="A4" style={simpleStyles.industrialPage}>
|
||||
<Header icon={headerIcon} sender={companyData.name} showAddress={false} />
|
||||
|
||||
<PDFView style={simpleStyles.contentView}>
|
||||
{children}
|
||||
</PDFView>
|
||||
|
||||
<Footer
|
||||
logo={footerLogo}
|
||||
companyData={companyData}
|
||||
bankData={bankData}
|
||||
showDetails={false}
|
||||
_bankData={bankData}
|
||||
showDetails={showDetails}
|
||||
showPageNumber={showPageNumber}
|
||||
/>
|
||||
</PDFPage>
|
||||
|
||||
53
packages/pdf-library/src/generate-agbs.ts
Normal file
53
packages/pdf-library/src/generate-agbs.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { renderToFile, Document as PDFDocument, Font } from "@react-pdf/renderer";
|
||||
import { createElement } from "react";
|
||||
import { AgbsPDF } from "./components/AgbsPDF.js";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
// Standard Font Registration
|
||||
Font.register({
|
||||
family: 'Outfit',
|
||||
fonts: [
|
||||
{ src: 'Helvetica' },
|
||||
{ src: 'Helvetica-Bold', fontWeight: 'bold' },
|
||||
],
|
||||
});
|
||||
|
||||
Font.register({
|
||||
family: 'Helvetica',
|
||||
fonts: [
|
||||
{ src: 'Helvetica' },
|
||||
{ src: 'Helvetica-Bold', fontWeight: 'bold' },
|
||||
],
|
||||
});
|
||||
|
||||
async function generate() {
|
||||
const outDir = path.join(__dirname, "../../../out");
|
||||
if (!fs.existsSync(outDir)) {
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
}
|
||||
|
||||
const outputPath = path.resolve(outDir, "AGB_Mintel.pdf");
|
||||
|
||||
console.log("Generating High-Fidelity AGB PDF...");
|
||||
|
||||
const headerIcon = "/Users/marcmintel/Projects/mintel.me/apps/web/src/assets/logo/Icon-White-Transparent.png";
|
||||
const footerLogo = "/Users/marcmintel/Projects/mintel.me/apps/web/src/assets/logo/Logo-Black-Transparent.png";
|
||||
|
||||
// WRAP IN DOCUMENT - MANDATORY FOR standalone rendering
|
||||
const document = createElement(PDFDocument, {
|
||||
title: "Allgemeine Geschäftsbedingungen - Marc Mintel",
|
||||
author: "Marc Mintel",
|
||||
},
|
||||
createElement(AgbsPDF, { mode: "full", headerIcon, footerLogo })
|
||||
);
|
||||
|
||||
await renderToFile(document, outputPath);
|
||||
console.log(`Generated: ${outputPath}`);
|
||||
}
|
||||
|
||||
generate().catch(console.error);
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "people-manager",
|
||||
"description": "Custom High-Fidelity Management for Directus",
|
||||
"icon": "extension",
|
||||
"version": "1.8.11",
|
||||
"version": "1.8.21",
|
||||
"type": "module",
|
||||
"keywords": [
|
||||
"directus",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@mintel/thumbnail-generator",
|
||||
"version": "1.8.11",
|
||||
"private": true,
|
||||
"version": "1.8.21",
|
||||
"private": false,
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
|
||||
@@ -56,8 +56,8 @@ export class ThumbnailGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
// Default to the requested nano-banana-pro model unless explicitly provided
|
||||
const model = options?.model || "google/nano-banana-pro";
|
||||
// Default to the requested flux-1.1-pro model unless explicitly provided
|
||||
const model = options?.model || "black-forest-labs/flux-1.1-pro";
|
||||
|
||||
const output = await this.replicate.run(model as `${string}/${string}`, {
|
||||
input: inputPayload,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mintel/tsconfig",
|
||||
"version": "1.8.11",
|
||||
"version": "1.8.21",
|
||||
"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.11",
|
||||
"version": "1.8.21",
|
||||
"type": "module",
|
||||
"keywords": [
|
||||
"directus",
|
||||
|
||||
14
plan_free.hbs
Normal file
14
plan_free.hbs
Normal file
@@ -0,0 +1,14 @@
|
||||
<div class="membership-card free">
|
||||
<div class="membership-card-content">
|
||||
<h2 class="membership-card-title">{{t "Free"}}</h2>
|
||||
<h3 class="membership-card-price"><sup>$</sup>0</h3>
|
||||
<div class="membership-card-options">
|
||||
<ul>
|
||||
<li>Full access to posts for subscribers</li>
|
||||
<li>Weekly email newsletter</li>
|
||||
<li>No advertising</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<a href="{{@site.url}}/signup/" class="global-button">{{t "Subscribe now"}}</a>
|
||||
</div>
|
||||
16
plan_monthly.hbs
Normal file
16
plan_monthly.hbs
Normal file
@@ -0,0 +1,16 @@
|
||||
<div class="membership-card monthly">
|
||||
<div class="membership-card-content">
|
||||
<h2 class="membership-card-title">{{t "Monthly"}}</h2>
|
||||
<h3 class="membership-card-price">{{price monthly_price currency=currency}}</h3>
|
||||
<div class="membership-card-options">
|
||||
<ul>
|
||||
<li>Full access to all premium posts</li>
|
||||
<li>Weekly email newsletter</li>
|
||||
<li>Support independent publishing</li>
|
||||
<li>Simple, secure card payment</li>
|
||||
<li>No advertising</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<a href="#" class="global-button" data-members-plan="Monthly">{{t "Subscribe now"}}</a>
|
||||
</div>
|
||||
17
plan_yearly.hbs
Normal file
17
plan_yearly.hbs
Normal file
@@ -0,0 +1,17 @@
|
||||
<div class="membership-card yearly">
|
||||
<div class="membership-card-content">
|
||||
<h2 class="membership-card-title">{{t "Yearly"}}</h2>
|
||||
<h3 class="membership-card-price">{{price yearly_price currency=currency}}</h3>
|
||||
<div class="membership-card-options">
|
||||
<ul>
|
||||
<li>Full access to all premium posts</li>
|
||||
<li>Weekly email newsletter</li>
|
||||
<li>Support independent publishing</li>
|
||||
<li>Simple, secure card payment</li>
|
||||
<li>One easy payment instead of 12!</li>
|
||||
<li>No advertising</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<a href="#" class="global-button" data-members-plan="Yearly">{{t "Subscribe now"}}</a>
|
||||
</div>
|
||||
611
pnpm-lock.yaml
generated
611
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -116,7 +116,6 @@ if (fs.existsSync(appsDir)) {
|
||||
}
|
||||
|
||||
// Update .env files
|
||||
updateEnv(".env");
|
||||
updateEnv(".env.example");
|
||||
|
||||
console.log("✨ All versions synced!");
|
||||
|
||||
Reference in New Issue
Block a user