fix
This commit is contained in:
@@ -1,162 +1,42 @@
|
||||
import { createBrowserContext } from '$lib/server/browser';
|
||||
import { createLLM } from '$lib/server/llm';
|
||||
import { extractTextAndThumbnail } from '$lib/server/extraction';
|
||||
import { extractRecipe } from '$lib/server/parser';
|
||||
import { json } from '@sveltejs/kit';
|
||||
import fs, { writeFileSync } from 'fs';
|
||||
import { zodResponseFormat } from 'openai/helpers/zod';
|
||||
import path from 'path';
|
||||
import { z } from 'zod';
|
||||
|
||||
const RecipeSchema = z.object({
|
||||
name: z.string(),
|
||||
servings: z.number().nullable(),
|
||||
description: z.string().nullable(),
|
||||
ingredients: z.array(z.object({
|
||||
item: z.string(),
|
||||
amount: z.string(),
|
||||
unit: z.string()
|
||||
})).nullable(),
|
||||
steps: z.array(z.string()).nullable(),
|
||||
image: z.string().nullable().optional()
|
||||
});
|
||||
|
||||
|
||||
export async function POST({ request }) {
|
||||
const { url } = await request.json();
|
||||
|
||||
// 1. Browser Connection - now managed by SvelteKit
|
||||
console.log('Creating browser context for URL:', url);
|
||||
const { url } = await request.json();
|
||||
|
||||
// Try to find auth storage
|
||||
const authPathDocker = '/app/secrets/auth.json';
|
||||
const authPathLocal = './secrets/auth.json';
|
||||
const authPath = fs.existsSync(authPathDocker) ? authPathDocker :
|
||||
fs.existsSync(authPathLocal) ? authPathLocal :
|
||||
undefined;
|
||||
console.log('Processing URL:', url);
|
||||
|
||||
const context = await createBrowserContext(authPath);
|
||||
const page = await context.newPage();
|
||||
|
||||
// Set a fixed viewport size (Instagram feed width)
|
||||
await page.setViewportSize({ width: 1080, height: 1920 });
|
||||
|
||||
let bodyText = '';
|
||||
let thumbnail: string | null = null;
|
||||
try {
|
||||
// Step 1: Extract text and thumbnail from page
|
||||
const { bodyText, thumbnail } = await extractTextAndThumbnail(url);
|
||||
|
||||
try {
|
||||
await page.goto(url, { waitUntil: 'domcontentloaded' });
|
||||
bodyText = (await page.evaluate(() => document.body.innerText)).replace(/^(?:.*\n){6}/, '').split('More posts from')[0].trim();
|
||||
bodyText = bodyText.replace(/@\w+/g, '').replace(/#\w+/g, '');
|
||||
// Step 2: Parse recipe from extracted text
|
||||
const recipe = await extractRecipe(bodyText);
|
||||
|
||||
writeFileSync(path.resolve('debug_page.txt'), bodyText); // Save for debugging, overwriting if exists
|
||||
const videoBounds = await page.evaluate(() => {
|
||||
const video = document.querySelector('video');
|
||||
if (!video) return null;
|
||||
const rect = video.getBoundingClientRect();
|
||||
return {
|
||||
x: Math.max(0, rect.left),
|
||||
y: Math.max(0, rect.top),
|
||||
width: Math.min(rect.width, window.innerWidth),
|
||||
height: Math.min(rect.height, window.innerHeight)
|
||||
};
|
||||
});
|
||||
if (!recipe) {
|
||||
return json({ error: 'No recipe found in provided text' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (videoBounds && videoBounds.width > 0 && videoBounds.height > 0) {
|
||||
const screenshotBuffer = await page.screenshot({
|
||||
type: 'jpeg',
|
||||
quality: 85,
|
||||
clip: videoBounds
|
||||
});
|
||||
thumbnail = `data:image/jpeg;base64,${screenshotBuffer.toString('base64')}`;
|
||||
} else {
|
||||
console.warn('Video element not found or has no size, taking full page screenshot');
|
||||
const screenshotBuffer = await page.screenshot({ type: 'jpeg', quality: 85 });
|
||||
thumbnail = `data:image/jpeg;base64,${screenshotBuffer.toString('base64')}`;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Scraping error:', e);
|
||||
return json({ error: 'Failed to scrape URL' }, { status: 500 });
|
||||
} finally {
|
||||
await page.close();
|
||||
await context.close();
|
||||
}
|
||||
// Step 3: Enrich recipe with metadata
|
||||
if (recipe.description) {
|
||||
recipe.description += `\n\nLink: ${url}`;
|
||||
} else {
|
||||
recipe.description = `Link: ${url}`;
|
||||
}
|
||||
|
||||
// 2. LLM Processing - Two-step validation
|
||||
try {
|
||||
const { client, model } = createLLM();
|
||||
|
||||
// STEP 1: Binary recipe detection (yes/no only)
|
||||
const detectionResponse = await client.chat.completions.create({
|
||||
model,
|
||||
messages: [
|
||||
{ role: "system", content: "You are a recipe detector. Answer with ONLY 'yes' or 'no' - nothing else. A recipe MUST have: (1) name/title, (2) ingredients with quantities, (3) numbered cooking steps. If ANY are missing, answer 'no'." },
|
||||
{ role: "user", content: `Does this text contain a recipe?\n\n${bodyText}` }
|
||||
],
|
||||
max_tokens: 10,
|
||||
});
|
||||
if (thumbnail) {
|
||||
recipe.image = thumbnail;
|
||||
}
|
||||
|
||||
const detectionResult = detectionResponse.choices[0].message.content?.toLowerCase() ?? '';
|
||||
const hasRecipe = detectionResult.includes("yes");
|
||||
return json({ recipe, bodyText });
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
console.error('Recipe extraction pipeline error:', errorMessage);
|
||||
|
||||
if (!hasRecipe) {
|
||||
return json({ error: "No recipe found in provided text" }, { status: 400 });
|
||||
}
|
||||
|
||||
// STEP 2: Extract recipe (only proceeds if recipe detected)
|
||||
const completion = await client.beta.chat.completions.parse({
|
||||
model,
|
||||
messages: [
|
||||
{ role: "system", content: `You are a RECIPE EXTRACTOR. Extract the recipe from the provided text.
|
||||
|
||||
✅ REQUIREMENTS:
|
||||
1. Extract the exact recipe name from the text
|
||||
2. List all ingredients with their quantities and units
|
||||
3. List all cooking steps in order
|
||||
4. Translate everything to Italian
|
||||
5. Convert measurements to SI units (g, mL, °C)
|
||||
|
||||
📋 CONVERSION TABLE:
|
||||
- 1 cup = 240 mL, 1 tbsp = 15 mL, 1 tsp = 5 mL
|
||||
- 1 oz = 28.35 g, 1 lb = 453.59 g
|
||||
- 1 stick butter = 113 g
|
||||
- °F→°C: (°F–32)×5/9
|
||||
|
||||
🔄 OUTPUT FORMAT:
|
||||
{
|
||||
"name": "recipe name in Italian",
|
||||
"servings": number or null,
|
||||
"description": "description in Italian or null",
|
||||
"ingredients": [{"item": "ingredient name", "amount": "quantity", "unit": "SI unit"}],
|
||||
"steps": ["1. First step", "2. Second step", ...]
|
||||
}
|
||||
|
||||
Extract ONLY what's explicitly in the text. Be accurate and literal.
|
||||
` },
|
||||
{ role: "user", content: `Extract the recipe from this text:\n\n${bodyText}` }
|
||||
],
|
||||
response_format: zodResponseFormat(RecipeSchema, "recipe")
|
||||
});
|
||||
console.log('LLM extraction successful:', completion.choices[0].message);
|
||||
|
||||
const recipe = completion.choices[0].message.parsed;
|
||||
if (!recipe || !recipe.name) {
|
||||
return json({ error: "Failed to extract recipe" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Append original Instagram link to description
|
||||
if (recipe.description) {
|
||||
recipe.description += `\n\nLink: ${url}`;
|
||||
} else {
|
||||
recipe.description = `Link: ${url}`;
|
||||
}
|
||||
|
||||
// Add thumbnail to recipe
|
||||
if (thumbnail) {
|
||||
recipe.image = thumbnail;
|
||||
}
|
||||
|
||||
return json({ recipe, bodyText });
|
||||
} catch (e) {
|
||||
console.error('LLM error:', e);
|
||||
return json({ error: 'Failed to parse recipe', bodyText }, { status: 500 });
|
||||
}
|
||||
return json(
|
||||
{ error: errorMessage || 'Failed to process URL' },
|
||||
{ status: error instanceof Error && error.message.includes('scrape') ? 500 : 400 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import {tandoorConfig} from '$lib/server/tandoor-config';
|
||||
export async function GET() {
|
||||
return json({...tandoorConfig, token: ''});
|
||||
}
|
||||
import { json } from '@sveltejs/kit';
|
||||
import {tandoorConfig} from '$lib/server/tandoor-config';
|
||||
export async function GET() {
|
||||
return json({...tandoorConfig, token: ''});
|
||||
}
|
||||
|
||||
@@ -1,42 +1,42 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { uploadRecipeWithIngredientsDTO, uploadRecipeImage } from '$lib/server/tandoor';
|
||||
|
||||
export async function POST({ request }) {
|
||||
const { recipe } = await request.json();
|
||||
|
||||
if (!recipe) {
|
||||
return json({ error: 'No recipe provided' }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await uploadRecipeWithIngredientsDTO(recipe);
|
||||
|
||||
if (!result.success) {
|
||||
return json({ error: result.error || 'Failed to upload recipe' }, { status: 500 });
|
||||
}
|
||||
|
||||
// Upload image if available
|
||||
let imageStatus = null;
|
||||
if (result.recipeId && result.imageUrl) {
|
||||
imageStatus = await uploadRecipeImage(result.recipeId, result.imageUrl);
|
||||
if (!imageStatus.success) {
|
||||
console.warn('Image upload failed, but recipe created:', imageStatus.error);
|
||||
}
|
||||
}
|
||||
|
||||
return json({
|
||||
success: true,
|
||||
message: 'Recipe successfully imported to Tandoor',
|
||||
recipeId: result.recipeId,
|
||||
imageUpload: imageStatus?.success ? 'successful' : 'failed'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Tandoor upload error:', error);
|
||||
return json(
|
||||
{
|
||||
error: error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { uploadRecipeWithIngredientsDTO, uploadRecipeImage } from '$lib/server/tandoor';
|
||||
|
||||
export async function POST({ request }) {
|
||||
const { recipe } = await request.json();
|
||||
|
||||
if (!recipe) {
|
||||
return json({ error: 'No recipe provided' }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await uploadRecipeWithIngredientsDTO(recipe);
|
||||
|
||||
if (!result.success) {
|
||||
return json({ error: result.error || 'Failed to upload recipe' }, { status: 500 });
|
||||
}
|
||||
|
||||
// Upload image if available
|
||||
let imageStatus = null;
|
||||
if (result.recipeId && result.imageUrl) {
|
||||
imageStatus = await uploadRecipeImage(result.recipeId, result.imageUrl);
|
||||
if (!imageStatus.success) {
|
||||
console.warn('Image upload failed, but recipe created:', imageStatus.error);
|
||||
}
|
||||
}
|
||||
|
||||
return json({
|
||||
success: true,
|
||||
message: 'Recipe successfully imported to Tandoor',
|
||||
recipeId: result.recipeId,
|
||||
imageUpload: imageStatus?.success ? 'successful' : 'failed'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Tandoor upload error:', error);
|
||||
return json(
|
||||
{
|
||||
error: error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user