ai impl
This commit is contained in:
parent
156e74450a
commit
90bbe4bfa4
2
.gitignore
vendored
2
.gitignore
vendored
@ -4,6 +4,8 @@ node_modules
|
||||
|
||||
uploads
|
||||
|
||||
llm-models
|
||||
|
||||
# Output
|
||||
.output
|
||||
.vercel
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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 @@
|
||||
</Field.Group>
|
||||
|
||||
<Dialog.Footer class="mt-4">
|
||||
<div class="flex justify-between w-full">
|
||||
<Button variant="destructive"
|
||||
onclick={() => {approveDenyItem({id: item?.id || 0, approved: false}); invalidateAll();}}>
|
||||
Delete
|
||||
</Button>
|
||||
|
||||
<div>
|
||||
|
||||
<Dialog.Close
|
||||
type="button"
|
||||
class={buttonVariants({ variant: "outline" })}
|
||||
>
|
||||
Cancel
|
||||
</Dialog.Close>
|
||||
<Button type="submit">Submit</Button>
|
||||
<Button type="submit" class="ml-1">Submit</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Footer>
|
||||
</form>
|
||||
{#if item?.threads}
|
||||
|
||||
@ -14,7 +14,6 @@
|
||||
export let previewUrl: string | null = null;
|
||||
let dragging = false;
|
||||
|
||||
console.log(previewUrl);
|
||||
|
||||
function openFileDialog() {
|
||||
if (!disabled) {
|
||||
@ -69,6 +68,7 @@
|
||||
disabled={disabled}
|
||||
capture="environment"
|
||||
on:change={onInputChange}
|
||||
formaction="/item?/describe"
|
||||
hidden
|
||||
/>
|
||||
|
||||
|
||||
@ -31,7 +31,7 @@
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="h-full bg-card text-card-foreground flex flex-col gap-2 rounded-xl border shadow-sm max-w-sm overflow-hidden min-2-3xs">
|
||||
class="h-full bg-card text-card-foreground flex flex-col gap-2 rounded-xl border shadow-sm max-w-sm overflow-hidden min-w-2xs">
|
||||
{#if item.image}
|
||||
<img src="https://fbla26.marinodev.com/uploads/{item.id}.jpg" class="object-cover min-h-48 max-h-48"
|
||||
alt="Lost item">
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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}/`
|
||||
// });
|
||||
// }
|
||||
|
||||
44
src/lib/llm/llm.server.ts
Normal file
44
src/lib/llm/llm.server.ts
Normal file
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -70,7 +70,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 grid-cols-[repeat(auto-fill,minmax(16rem,max-content))] justify-center">
|
||||
{#if data.items.length === 0}
|
||||
|
||||
<h1 class="text-xl text-center font-semibold mt-16">No items found!</h1>
|
||||
<p class="text-center">Try broadening your search terms to get more results.</p>
|
||||
{:else}
|
||||
<div class="grid gap-4 grid-cols-[repeat(auto-fill,minmax(18rem,max-content))] justify-center">
|
||||
|
||||
{#if data.user && data.items.some(
|
||||
(item) => item.approvedDate === null
|
||||
@ -88,7 +93,7 @@
|
||||
{/each}
|
||||
|
||||
{#if data.user && data.items.some(
|
||||
(item) => item.approvedDate !== null
|
||||
(item) => item.approvedDate !== null && item.claimedDate === null
|
||||
)}
|
||||
<FieldSeparator class="col-span-full text-lg my-2 h-8 w-full">
|
||||
Public items
|
||||
@ -102,7 +107,22 @@
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
{#if data.user && data.items.some(
|
||||
(item) => item.claimedDate !== null
|
||||
)}
|
||||
<FieldSeparator class="col-span-full text-lg my-2 h-8 w-full">
|
||||
Claimed items
|
||||
</FieldSeparator>
|
||||
{#each data.items as item (item.id)}
|
||||
{#if item.claimedDate !== null}
|
||||
<ItemListing item={item} admin={data.user !== null} editCallback={openEditDialog}
|
||||
inquireCallback={openInquireDialog} claimCallback={openClaimDialog} />
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<Button class="fixed shadow-lg bottom-6 right-6 rounded-xl md:hidden p-6 text-xl" size="default"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user