d2ac67fb44
Add type annotations, JSDoc types, null checks, and proper generics to eliminate all svelte-check errors. Key changes include: - Type $state(null) variables to avoid 'never' inference - Add JSDoc typedefs for plain <script> components - Fix mongoose model typing with Model<any> to avoid union complexity - Add App.Error/App.PageState interfaces in app.d.ts - Fix tuple types to array types in types.ts - Type catch block errors and API handler params - Add null safety for DOM queries and optional chaining - Add standard line-clamp property alongside -webkit- prefix
184 lines
4.5 KiB
TypeScript
184 lines
4.5 KiB
TypeScript
import { json, error } from '@sveltejs/kit';
|
|
import type { RequestHandler } from './$types';
|
|
import { generateAltText, type RecipeContext } from '$lib/server/ai/alttext.js';
|
|
import { checkOllamaHealth } from '$lib/server/ai/ollama.js';
|
|
import { Recipe } from '$models/Recipe.js';
|
|
|
|
export const POST: RequestHandler = async ({ request, locals }) => {
|
|
// Check authentication
|
|
const session = await locals.auth();
|
|
if (!session?.user) {
|
|
throw error(401, 'Unauthorized');
|
|
}
|
|
|
|
try {
|
|
const body = await request.json();
|
|
const { filter = 'missing', limit = 10, modelName } = body;
|
|
|
|
// Check if Ollama is available
|
|
const isOllamaAvailable = await checkOllamaHealth();
|
|
if (!isOllamaAvailable) {
|
|
throw error(503, 'Ollama service is not available. Make sure Ollama is running.');
|
|
}
|
|
|
|
// Build query based on filter
|
|
let query: any = { images: { $exists: true, $ne: [] } };
|
|
|
|
if (filter === 'missing') {
|
|
// Find recipes with images but missing alt text
|
|
query = {
|
|
images: {
|
|
$elemMatch: {
|
|
mediapath: { $exists: true },
|
|
$or: [{ alt: { $exists: false } }, { alt: '' }],
|
|
},
|
|
},
|
|
};
|
|
} else if (filter === 'all') {
|
|
// Process all recipes with images
|
|
query = { images: { $exists: true, $ne: [] } };
|
|
}
|
|
|
|
// Fetch recipes
|
|
const recipes = await Recipe.find(query).limit(limit);
|
|
|
|
if (recipes.length === 0) {
|
|
return json({
|
|
success: true,
|
|
processed: 0,
|
|
message: 'No recipes found matching criteria',
|
|
});
|
|
}
|
|
|
|
const results: Array<{
|
|
shortName: string;
|
|
name: string;
|
|
processed: number;
|
|
failed: number;
|
|
}> = [];
|
|
|
|
// Process each recipe
|
|
for (const recipe of recipes) {
|
|
let processed = 0;
|
|
let failed = 0;
|
|
|
|
for (let i = 0; i < recipe.images.length; i++) {
|
|
const image = recipe.images[i];
|
|
|
|
// Skip if alt text exists and we're only processing missing ones
|
|
if (filter === 'missing' && image.alt) {
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
// Prepare context
|
|
const context: RecipeContext = {
|
|
name: recipe.name,
|
|
category: recipe.category,
|
|
tags: recipe.tags,
|
|
};
|
|
|
|
// Generate alt text
|
|
const altTextResult = await generateAltText(
|
|
image.mediapath,
|
|
context,
|
|
modelName || 'gemma3:latest'
|
|
);
|
|
|
|
// Update German alt text
|
|
recipe.images[i].alt = altTextResult.de;
|
|
|
|
// Ensure translations.en.images array exists
|
|
if (!recipe.translations) {
|
|
(recipe as any).translations = { en: { images: [] } };
|
|
}
|
|
if (!recipe.translations!.en) {
|
|
(recipe.translations as any).en = { images: [] };
|
|
}
|
|
if (!recipe.translations!.en!.images) {
|
|
(recipe.translations!.en as any).images = [];
|
|
}
|
|
|
|
// Ensure array has enough entries
|
|
while (recipe.translations!.en!.images!.length <= i) {
|
|
recipe.translations!.en!.images!.push({ alt: '', caption: '' } as any);
|
|
}
|
|
|
|
// Update English alt text
|
|
recipe.translations!.en!.images![i].alt = altTextResult.en;
|
|
|
|
processed++;
|
|
} catch (err) {
|
|
console.error(`Failed to process image ${i} for recipe ${recipe.short_name}:`, err);
|
|
failed++;
|
|
}
|
|
}
|
|
|
|
// Save recipe if any images were processed
|
|
if (processed > 0) {
|
|
await recipe.save();
|
|
}
|
|
|
|
results.push({
|
|
shortName: recipe.short_name,
|
|
name: recipe.name,
|
|
processed,
|
|
failed,
|
|
});
|
|
}
|
|
|
|
return json({
|
|
success: true,
|
|
processed: results.reduce((sum, r) => sum + r.processed, 0),
|
|
failed: results.reduce((sum, r) => sum + r.failed, 0),
|
|
results,
|
|
});
|
|
} catch (err) {
|
|
console.error('Error in bulk alt text generation:', err);
|
|
|
|
if (err instanceof Error && 'status' in err) {
|
|
throw err;
|
|
}
|
|
|
|
throw error(500, err instanceof Error ? err.message : 'Failed to generate alt text');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* GET endpoint to check status and get stats
|
|
*/
|
|
export const GET: RequestHandler = async ({ locals }) => {
|
|
// Check authentication
|
|
const session = await locals.auth();
|
|
if (!session?.user) {
|
|
throw error(401, 'Unauthorized');
|
|
}
|
|
|
|
try {
|
|
// Count recipes with missing alt text
|
|
const totalWithImages = await Recipe.countDocuments({
|
|
images: { $exists: true, $ne: [] },
|
|
});
|
|
|
|
const missingAltText = await Recipe.countDocuments({
|
|
images: {
|
|
$elemMatch: {
|
|
mediapath: { $exists: true },
|
|
$or: [{ alt: { $exists: false } }, { alt: '' }],
|
|
},
|
|
},
|
|
});
|
|
|
|
// Check Ollama health
|
|
const ollamaAvailable = await checkOllamaHealth();
|
|
|
|
return json({
|
|
totalWithImages,
|
|
missingAltText,
|
|
ollamaAvailable,
|
|
});
|
|
} catch (err) {
|
|
throw error(500, 'Failed to fetch statistics');
|
|
}
|
|
};
|