diff --git a/.gitignore b/.gitignore
index 40af263..552bc4e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,6 +4,8 @@ node_modules
uploads
+llm-models
+
# Output
.output
.vercel
diff --git a/docker-compose.yml b/docker-compose.yml
index f25671d..24b7331 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -13,7 +13,7 @@ services:
- fbla26
fbla26:
image: git.marinodev.com/marinodev/fbla26_ci:latest
- restart: on-failure
+ restart: unless-stopped
env_file: ".env"
ports:
- "${FBLA26_PORT}:3000"
@@ -22,6 +22,29 @@ services:
volumes:
- ./.env:/srv/fbla26/.env
- /var/fbla26/uploads/:/srv/fbla26/uploads/
+ llama:
+ image: ghcr.io/ggml-org/llama.cpp:server
+ restart: unless-stopped
+ env_file: ".env"
+ ports:
+ - "${LLAMA_PORT}:8080"
+ networks:
+ - fbla26
+ volumes:
+ - /home/drake/llama/models:/models:ro
+ command:
+ - -m
+ - /models/Qwen3VL-2B-Instruct-Q4_K_M.gguf
+ - --mmproj
+ - /models/mmproj-Qwen3VL-2B-Instruct-Q8_0.gguf
+ - --host
+ - 0.0.0.0
+ - --port
+ - "8080"
+ - -n
+ - "512"
+
+
networks:
fbla26:
diff --git a/src/lib/components/custom/edit-item-dialog.svelte b/src/lib/components/custom/edit-item-dialog.svelte
index 8ade4c6..67858c5 100644
--- a/src/lib/components/custom/edit-item-dialog.svelte
+++ b/src/lib/components/custom/edit-item-dialog.svelte
@@ -8,7 +8,7 @@
import { Separator } from '$lib/components/ui/separator';
import TrashIcon from '@lucide/svelte/icons/trash';
import ImageUpload from '$lib/components/custom/image-upload/image-upload.svelte';
- import { genDescription } from '$lib/db/items.remote';
+ import { approveDenyItem, genDescription } from '$lib/db/items.remote';
import { EMAIL_REGEX_STRING } from '$lib/consts';
import type { Item } from '$lib/types/item.server';
import { Textarea } from '$lib/components/ui/textarea';
@@ -26,9 +26,9 @@
let description: string = $derived(item ? item.description : '');
let isGenerating = $state(false);
- async function onSelect() {
+ async function onSelect(file: File) {
isGenerating = true;
- description = await genDescription();
+ description = await genDescription(file);
isGenerating = false;
}
@@ -110,13 +110,23 @@
diff --git a/src/lib/components/custom/submit-item-dialog.svelte b/src/lib/components/custom/submit-item-dialog.svelte
index 548b8ac..147bedd 100644
--- a/src/lib/components/custom/submit-item-dialog.svelte
+++ b/src/lib/components/custom/submit-item-dialog.svelte
@@ -16,9 +16,15 @@
let description: string | undefined = $state();
let isGenerating = $state(false);
- async function onSelect() {
+ async function onSelect(file: File) {
isGenerating = true;
- description = await genDescription();
+
+ const buffer = await file.arrayBuffer();
+ const base64 = btoa(
+ String.fromCharCode(...new Uint8Array(buffer))
+ );
+
+ description = await genDescription(base64);
isGenerating = false;
}
diff --git a/src/lib/db/items.remote.ts b/src/lib/db/items.remote.ts
index fea83be..8bd49f8 100644
--- a/src/lib/db/items.remote.ts
+++ b/src/lib/db/items.remote.ts
@@ -1,12 +1,31 @@
-import { command, getRequestEvent, query } from '$app/server';
+import { command, getRequestEvent } from '$app/server';
import * as v from 'valibot';
import sql from '$lib/db/db.server';
import { verifyJWT } from '$lib/auth/index.server';
+import sharp from 'sharp';
+import { LLMDescribe } from '$lib/llm/llm.server';
-export const genDescription = query(async () => {
- await new Promise((f) => setTimeout(f, 1000));
+export const genDescription = command(v.string(), async (data) => {
+ const inputBuffer = Buffer.from(data, 'base64');
- return 'A matte black water bottle with a black lid and a "BKLYN BENTO" logo on the side, resting on a tree trunk in a forest.';
+ // Convert File → Buffer
+ // const inputBuffer = Buffer.from(await file.arrayBuffer());
+
+ // Detect format (Sharp does this internally)
+ const image = sharp(inputBuffer).rotate();
+
+ const outputBuffer = await image
+ .rotate()
+ .resize({ fit: 'outside', height: 400, width: 400 })
+ .jpeg({ quality: 80 }) // adjust if needed
+ .toBuffer();
+
+ const description = await LLMDescribe(
+ `data:image/jpeg;base64,${outputBuffer.toString('base64')}`
+ );
+ console.log(description);
+
+ return description;
});
export const approveDenyItem = command(
diff --git a/src/lib/email/sender.server.ts b/src/lib/email/sender.server.ts
index 8a986b8..37be8dc 100644
--- a/src/lib/email/sender.server.ts
+++ b/src/lib/email/sender.server.ts
@@ -1,7 +1,7 @@
import nodemailer from 'nodemailer';
// Create a transporter object using SMTP transport
-const transporter = nodemailer.createTransport({
+export const transporter = nodemailer.createTransport({
host: process.env.EMAIL_HOST,
port: Number(process.env.EMAIL_PORT),
secure: true, // true for 465, false for other ports
@@ -11,13 +11,13 @@ const transporter = nodemailer.createTransport({
}
});
-export async function sendEmployerNotificationEmail() {
- // Send mail with defined transport object
- await transporter.sendMail({
- from: `CareerConnect Notifications <${process.env.EMAIL_USER}>`,
- // to: info.emails.join(', '), // EMAILING OF REAL COMPANIES DISABLED, UNCOMMENT TO ENABLE
- to: 'drake@marinodev.com', // TEMPORARY EMAIL FOR TESTING
- subject: 'New Application Received!',
- text: `A new application has been received for the posting ''!\n\nCheck it out at ${process.env.BASE_URL}/`
- });
-}
+// export async function sendEmail() {
+// // Send mail with defined transport object
+// await transporter.sendMail({
+// from: `Westuffind Notifier <${process.env.EMAIL_USER}>`,
+// // to: info.emails.join(', '), // EMAILING OF REAL COMPANIES DISABLED, UNCOMMENT TO ENABLE
+// to: 'drake@marinodev.com', // TEMPORARY EMAIL FOR TESTING
+// subject: 'New Application Received!',
+// text: `A new application has been received for the posting ''!\n\nCheck it out at ${process.env.BASE_URL}/`
+// });
+// }
diff --git a/src/lib/llm/llm.server.ts b/src/lib/llm/llm.server.ts
new file mode 100644
index 0000000..113e72d
--- /dev/null
+++ b/src/lib/llm/llm.server.ts
@@ -0,0 +1,44 @@
+export async function LLMDescribe(imageData: string) {
+ const payload = {
+ messages: [
+ {
+ role: 'user',
+ content: [
+ {
+ type: 'text',
+ text: 'Describe only the main object in this image in one sentence. Include its color, shape, brand, or any text on it. Focus solely on the object itself. Example: A blue Hydroflask water bottle with a dented lid and a sticker of a mountain on the side.'
+ },
+ {
+ type: 'image_url',
+ image_url: {
+ url: imageData
+ }
+ }
+ ]
+ }
+ ],
+ max_tokens: 128,
+ temperature: 0.2
+ };
+
+ console.log('AIing it');
+
+ const res = await fetch(
+ `http://${process.env.LLAMA_HOST!}:${process.env.LLAMA_PORT!}/v1/chat/completions`,
+ {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify(payload)
+ }
+ );
+
+ if (!res.ok) {
+ console.error(await res.text());
+ process.exit(1);
+ }
+
+ const data = await res.json();
+ return data.choices[0].message.content;
+}
diff --git a/src/routes/items/+page.server.ts b/src/routes/items/+page.server.ts
index 5b2d8f8..acce700 100644
--- a/src/routes/items/+page.server.ts
+++ b/src/routes/items/+page.server.ts
@@ -8,7 +8,7 @@ import type { Item } from '$lib/types/item.server';
import { getFormString, getRequiredFormString } from '$lib/shared';
export const load: PageServerLoad = async ({ url, locals }) => {
- const searchQuery = url.searchParams.get('search') as string;
+ const searchQuery = url.searchParams.get('search');
// If the user is logged in, fetch items together with their threads (each thread contains its first message)
if (locals && locals.user) {
@@ -33,6 +33,7 @@ export const load: PageServerLoad = async ({ url, locals }) => {
FROM inquiry_messages im
WHERE im.thread_id = t.id
) m ON TRUE
+ ${searchQuery ? sql`WHERE word_similarity(${searchQuery}, i.description) > 0.3` : sql``}
GROUP BY i.id;
`;
items.forEach((item) =>
@@ -265,7 +266,6 @@ export const actions: Actions = {
let outputBuffer: Buffer | undefined;
if (file) {
- console.log(file);
if (!(file instanceof File) || file.size === 0) {
return fail(400, { message: 'No file uploaded or file is invalid', success: false });
}
@@ -315,7 +315,6 @@ export const actions: Actions = {
inquire: async ({ request, url }) => {
const id = url.searchParams.get('id');
if (!id) {
- console.log('No id provided!');
return fail(400, { message: 'Missing id!', success: false });
}
@@ -370,4 +369,29 @@ export const actions: Actions = {
});
}
}
+ // describe: async ({ request }) => {
+ // const data = await request.formData();
+ //
+ // let image = data.get('image');
+ // if (!(image instanceof File) || image.size === 0) {
+ // return fail(400, { message: 'No file uploaded or file is invalid', success: false });
+ // }
+ //
+ // // Convert File → Buffer
+ // const inputBuffer = Buffer.from(await file.arrayBuffer());
+ //
+ // // Detect format (Sharp does this internally)
+ // const image = sharp(inputBuffer).rotate();
+ //
+ // const outputBuffer = await image
+ // .rotate()
+ // .resize({ fit: 'outside', height: 400, width: 400 })
+ // .jpeg({ quality: 90 }) // adjust if needed
+ // .toBuffer();
+ //
+ // const description = LLMDescribe(`data:image/jpeg;base64,${outputBuffer.toString('base64')}`);
+ // console.log(description);
+ //
+ // return description;
+ // }
} satisfies Actions;
diff --git a/src/routes/items/+page.svelte b/src/routes/items/+page.svelte
index cbc4c33..05f259d 100644
--- a/src/routes/items/+page.svelte
+++ b/src/routes/items/+page.svelte
@@ -70,39 +70,59 @@
Try broadening your search terms to get more results.
+ {:else} +