From c00f4e5ea591ad380eee9e77ce05a88629985924 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Sun, 22 Feb 2026 22:14:35 +0100 Subject: [PATCH] fix(image-service): resolve next.js build crash and strict TS lint warnings for ci deploy --- .env | 2 +- apps/sample-website/next.config.ts | 9 +- .../sample-website/src/app/api/image/route.ts | 87 +++++++++++-------- packages/journaling/src/agent.ts | 9 +- packages/meme-generator/src/index.ts | 2 +- 5 files changed, 65 insertions(+), 44 deletions(-) diff --git a/.env b/.env index ea06df3..65e380c 100644 --- a/.env +++ b/.env @@ -1,5 +1,5 @@ # Project -IMAGE_TAG=v1.8.6 +IMAGE_TAG=v1.8.11 PROJECT_NAME=at-mintel PROJECT_COLOR=#82ed20 GITEA_TOKEN=ccce002e30fe16a31a6c9d5a414740af2f72a582 diff --git a/apps/sample-website/next.config.ts b/apps/sample-website/next.config.ts index fddd24a..3c25a51 100644 --- a/apps/sample-website/next.config.ts +++ b/apps/sample-website/next.config.ts @@ -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); diff --git a/apps/sample-website/src/app/api/image/route.ts b/apps/sample-website/src/app/api/image/route.ts index 0586551..bc23b77 100644 --- a/apps/sample-website/src/app/api/image/route.ts +++ b/apps/sample-website/src/app/api/image/route.ts @@ -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 }, + ); + } } diff --git a/packages/journaling/src/agent.ts b/packages/journaling/src/agent.ts index bcbba86..133baac 100644 --- a/packages/journaling/src/agent.ts +++ b/packages/journaling/src/agent.ts @@ -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: [] }; diff --git a/packages/meme-generator/src/index.ts b/packages/meme-generator/src/index.ts index c672f76..765d241 100644 --- a/packages/meme-generator/src/index.ts +++ b/packages/meme-generator/src/index.ts @@ -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 []; }