first-update
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getImageDetailWithQuestions } from '@/lib/services/images';
|
||||
|
||||
// 根据图片ID获取图片详情,包含问题列表和已标注数据
|
||||
export async function GET(request, { params }) {
|
||||
try {
|
||||
const { projectId, imageId } = params;
|
||||
|
||||
// 调用服务层获取图片详情
|
||||
const imageData = await getImageDetailWithQuestions(projectId, imageId);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: imageData
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to get image details:', error);
|
||||
|
||||
// 根据错误类型返回不同的状态码
|
||||
let statusCode = 500;
|
||||
if (error.message === '缺少图片ID') {
|
||||
statusCode = 400;
|
||||
} else if (error.message === '图片不存在') {
|
||||
statusCode = 404;
|
||||
} else if (error.message === '图片不属于指定项目') {
|
||||
statusCode = 403;
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: error.message || 'Failed to get image details' }, { status: statusCode });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { getImageById, getImageChunk } from '@/lib/db/images';
|
||||
import { createImageDataset } from '@/lib/db/imageDatasets';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
// 创建标注
|
||||
export async function POST(request, { params }) {
|
||||
try {
|
||||
const { projectId } = params;
|
||||
const { imageId, questionId, question, answerType, answer, note } = await request.json();
|
||||
|
||||
// 验证必填字段
|
||||
if (!imageId || !question || !answerType || answer === undefined || answer === null) {
|
||||
return NextResponse.json({ error: '缺少必要参数:imageId, question, answerType, answer' }, { status: 400 });
|
||||
}
|
||||
|
||||
// 验证图片存在
|
||||
const image = await getImageById(imageId);
|
||||
if (!image || image.projectId !== projectId) {
|
||||
return NextResponse.json({ error: '图片不存在' }, { status: 404 });
|
||||
}
|
||||
|
||||
// 验证答案类型
|
||||
if (!['text', 'label', 'custom_format'].includes(answerType)) {
|
||||
return NextResponse.json({ error: '无效的答案类型' }, { status: 400 });
|
||||
}
|
||||
|
||||
// 验证答案内容
|
||||
if (answerType === 'text' && typeof answer !== 'string') {
|
||||
return NextResponse.json({ error: '文本类型答案必须是字符串' }, { status: 400 });
|
||||
}
|
||||
if (answerType === 'label' && !Array.isArray(answer)) {
|
||||
return NextResponse.json({ error: '标签类型答案必须是数组' }, { status: 400 });
|
||||
}
|
||||
|
||||
// 序列化答案
|
||||
let answerString = answer;
|
||||
if (answerType !== 'text' && typeof answerString !== 'string') {
|
||||
answerString = JSON.stringify(answer, null, 2);
|
||||
}
|
||||
|
||||
// 1. 获取问题记录(前端传递的 questionId 指向已有的问题)
|
||||
if (!questionId) {
|
||||
return NextResponse.json({ error: '缺少必要参数:questionId' }, { status: 400 });
|
||||
}
|
||||
|
||||
const questionRecord = await prisma.questions.findUnique({
|
||||
where: { id: questionId }
|
||||
});
|
||||
|
||||
if (!questionRecord) {
|
||||
return NextResponse.json({ error: '问题不存在' }, { status: 404 });
|
||||
}
|
||||
|
||||
// 验证问题属于该图片
|
||||
if (questionRecord.imageId !== imageId) {
|
||||
return NextResponse.json({ error: '问题不属于该图片' }, { status: 400 });
|
||||
}
|
||||
|
||||
// 2. 更新问题为已回答
|
||||
await prisma.questions.update({
|
||||
where: { id: questionRecord.id },
|
||||
data: { answered: true }
|
||||
});
|
||||
|
||||
// 3. 创建 ImageDataset 记录
|
||||
const dataset = await createImageDataset(projectId, {
|
||||
imageId: image.id,
|
||||
imageName: image.imageName,
|
||||
questionId: questionRecord.id,
|
||||
question,
|
||||
answer: answerString,
|
||||
answerType,
|
||||
model: 'manual',
|
||||
note: note || ''
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
dataset,
|
||||
questionId: questionRecord.id
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to create annotation:', error);
|
||||
return NextResponse.json({ error: error.message || 'Failed to create annotation' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getImageByName } from '@/lib/db/images';
|
||||
import imageService from '@/lib/services/images';
|
||||
|
||||
// 生成图像数据集
|
||||
export async function POST(request, { params }) {
|
||||
try {
|
||||
const { projectId } = params;
|
||||
const { imageName, question, model, language = 'zh', previewOnly = false } = await request.json();
|
||||
|
||||
if (!imageName || !question) {
|
||||
return NextResponse.json({ error: '缺少必要参数' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!model) {
|
||||
return NextResponse.json({ error: '请选择一个视觉模型' }, { status: 400 });
|
||||
}
|
||||
|
||||
// 获取图片信息
|
||||
const image = await getImageByName(projectId, imageName);
|
||||
if (!image) {
|
||||
return NextResponse.json({ error: '图片不存在' }, { status: 404 });
|
||||
}
|
||||
|
||||
// 调用图片数据集生成服务
|
||||
const result = await imageService.generateDatasetForImage(projectId, image.id, question, {
|
||||
model,
|
||||
language,
|
||||
previewOnly
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
answer: result.answer,
|
||||
dataset: result.dataset
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to generate image dataset:', error);
|
||||
return NextResponse.json({ error: error.message || 'Failed to generate dataset' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { getImageDetailWithQuestions } from '@/lib/services/images';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
// 获取下一个有未标注问题的图片
|
||||
export async function GET(request, { params }) {
|
||||
try {
|
||||
const { projectId } = params;
|
||||
|
||||
// 查找第一个有未标注问题的图片
|
||||
const unansweredQuestion = await prisma.questions.findFirst({
|
||||
where: {
|
||||
projectId,
|
||||
imageId: {
|
||||
not: null
|
||||
},
|
||||
answered: false
|
||||
}
|
||||
});
|
||||
|
||||
if (!unansweredQuestion) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: null
|
||||
});
|
||||
}
|
||||
|
||||
// 调用服务层获取图片详情
|
||||
const imageData = await getImageDetailWithQuestions(projectId, unansweredQuestion.imageId);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: imageData
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to get next unanswered image:', error);
|
||||
return NextResponse.json({ error: error.message || 'Failed to get next unanswered image' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getProjectPath } from '@/lib/db/base';
|
||||
import { importImagesFromDirectories } from '@/lib/services/images';
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import { savePdfAsImages } from '@/lib/util/file';
|
||||
|
||||
// PDF 转图片并导入
|
||||
export async function POST(request, { params }) {
|
||||
let tempPdfPath = null;
|
||||
let tempImagesDir = null;
|
||||
|
||||
try {
|
||||
const { projectId } = params;
|
||||
const formData = await request.formData();
|
||||
const pdfFile = formData.get('file');
|
||||
|
||||
if (!pdfFile) {
|
||||
return NextResponse.json({ error: '请选择 PDF 文件' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!pdfFile.name.toLowerCase().endsWith('.pdf')) {
|
||||
return NextResponse.json({ error: '只支持 PDF 文件' }, { status: 400 });
|
||||
}
|
||||
|
||||
const projectPath = await getProjectPath(projectId);
|
||||
const tempDir = path.join(projectPath, 'temp');
|
||||
await fs.mkdir(tempDir, { recursive: true });
|
||||
|
||||
// 1. 保存 PDF 到临时目录
|
||||
tempPdfPath = path.join(tempDir, `temp_${Date.now()}_${pdfFile.name}`);
|
||||
const pdfBuffer = Buffer.from(await pdfFile.arrayBuffer());
|
||||
await fs.writeFile(tempPdfPath, pdfBuffer);
|
||||
|
||||
// 2. 创建临时图片目录
|
||||
tempImagesDir = path.join(tempDir, `pdf_images_${Date.now()}`);
|
||||
await fs.mkdir(tempImagesDir, { recursive: true });
|
||||
|
||||
// 3. 调用 pdf2md-js 转换 PDF 为图片
|
||||
console.log('开始转换 PDF 为图片...');
|
||||
const imagePaths = await savePdfAsImages(tempPdfPath, tempImagesDir, 3);
|
||||
console.log('PDF 转换完成,生成图片数量:', imagePaths.length);
|
||||
|
||||
if (!imagePaths || imagePaths.length === 0) {
|
||||
throw new Error('PDF 转换失败,未生成图片');
|
||||
}
|
||||
|
||||
// 4. 直接调用服务层导入图片
|
||||
const importResult = await importImagesFromDirectories(projectId, [tempImagesDir]);
|
||||
|
||||
// 5. 清理临时文件
|
||||
try {
|
||||
if (tempPdfPath) {
|
||||
await fs.unlink(tempPdfPath);
|
||||
}
|
||||
if (tempImagesDir) {
|
||||
const tempImages = await fs.readdir(tempImagesDir);
|
||||
for (const img of tempImages) {
|
||||
await fs.unlink(path.join(tempImagesDir, img));
|
||||
}
|
||||
await fs.rmdir(tempImagesDir);
|
||||
}
|
||||
const tempDirContents = await fs.readdir(tempDir);
|
||||
if (tempDirContents.length === 0) {
|
||||
await fs.rmdir(tempDir);
|
||||
}
|
||||
} catch (cleanupErr) {
|
||||
console.warn('清理临时文件失败:', cleanupErr);
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
count: importResult.count,
|
||||
images: importResult.images,
|
||||
pdfName: pdfFile.name
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to convert PDF:', error);
|
||||
|
||||
// 清理临时文件
|
||||
try {
|
||||
if (tempPdfPath) {
|
||||
await fs.unlink(tempPdfPath).catch(() => {});
|
||||
}
|
||||
if (tempImagesDir) {
|
||||
const tempImages = await fs.readdir(tempImagesDir).catch(() => []);
|
||||
for (const img of tempImages) {
|
||||
await fs.unlink(path.join(tempImagesDir, img)).catch(() => {});
|
||||
}
|
||||
await fs.rmdir(tempImagesDir).catch(() => {});
|
||||
}
|
||||
} catch (cleanupErr) {
|
||||
console.warn('清理临时文件失败:', cleanupErr);
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: error.message || 'Failed to convert PDF' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getImageByName } from '@/lib/db/images';
|
||||
import imageService from '@/lib/services/images';
|
||||
|
||||
// 生成图片问题
|
||||
export async function POST(request, { params }) {
|
||||
try {
|
||||
const { projectId } = params;
|
||||
const { imageName, count = 3, model, language = 'zh' } = await request.json();
|
||||
|
||||
if (!imageName) {
|
||||
return NextResponse.json({ error: '缺少图片名称' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!model) {
|
||||
return NextResponse.json({ error: '请选择一个视觉模型' }, { status: 400 });
|
||||
}
|
||||
|
||||
// 获取图片信息
|
||||
const image = await getImageByName(projectId, imageName);
|
||||
if (!image) {
|
||||
return NextResponse.json({ error: '图片不存在' }, { status: 404 });
|
||||
}
|
||||
|
||||
// 调用图片问题生成服务
|
||||
const result = await imageService.generateQuestionsForImage(projectId, image.id, {
|
||||
model,
|
||||
language,
|
||||
count
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
questions: result.questions
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to generate image questions:', error);
|
||||
return NextResponse.json({ error: error.message || 'Failed to generate questions' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getImages, deleteImage, getImageDetail } from '@/lib/db/images';
|
||||
import { getProjectPath } from '@/lib/db/base';
|
||||
import { db } from '@/lib/db/index';
|
||||
import { importImagesFromDirectories } from '@/lib/services/images';
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
|
||||
// 获取图片列表
|
||||
export async function GET(request, { params }) {
|
||||
try {
|
||||
const { projectId } = params;
|
||||
const { searchParams } = new URL(request.url);
|
||||
|
||||
const page = parseInt(searchParams.get('page')) || 1;
|
||||
const pageSize = parseInt(searchParams.get('pageSize')) || 20;
|
||||
const imageName = searchParams.get('imageName') || '';
|
||||
const hasQuestions = searchParams.get('hasQuestions');
|
||||
const hasDatasets = searchParams.get('hasDatasets');
|
||||
const simple = searchParams.get('simple');
|
||||
|
||||
const result = await getImages(projectId, page, pageSize, imageName, hasQuestions, hasDatasets, simple);
|
||||
|
||||
return NextResponse.json(result);
|
||||
} catch (error) {
|
||||
console.error('Failed to get images:', error);
|
||||
return NextResponse.json({ error: error.message || 'Failed to get images' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// 导入图片
|
||||
export async function POST(request, { params }) {
|
||||
try {
|
||||
const { projectId } = params;
|
||||
const { directories } = await request.json();
|
||||
|
||||
// 调用服务层处理图片导入
|
||||
const result = await importImagesFromDirectories(projectId, directories);
|
||||
|
||||
return NextResponse.json(result);
|
||||
} catch (error) {
|
||||
console.error('Failed to import images:', error);
|
||||
return NextResponse.json({ error: error.message || 'Failed to import images' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// 删除图片
|
||||
export async function DELETE(request, { params }) {
|
||||
try {
|
||||
const { projectId } = params;
|
||||
const { searchParams } = new URL(request.url);
|
||||
const imageId = searchParams.get('imageId');
|
||||
|
||||
if (!imageId) {
|
||||
return NextResponse.json({ error: '缺少图片ID' }, { status: 400 });
|
||||
}
|
||||
|
||||
// 获取图片信息
|
||||
const image = await getImageDetail(imageId);
|
||||
|
||||
if (!image) {
|
||||
return NextResponse.json({ error: '图片不存在' }, { status: 404 });
|
||||
}
|
||||
|
||||
// 删除关联的数据集
|
||||
await db.imageDatasets.deleteMany({
|
||||
where: { imageId }
|
||||
});
|
||||
|
||||
// 删除关联的问题
|
||||
await db.questions.deleteMany({
|
||||
where: { imageId }
|
||||
});
|
||||
|
||||
// 删除文件
|
||||
const projectPath = await getProjectPath(projectId);
|
||||
const filePath = path.join(projectPath, 'images', image.imageName);
|
||||
try {
|
||||
await fs.unlink(filePath);
|
||||
} catch (err) {
|
||||
console.warn('删除文件失败:', err);
|
||||
}
|
||||
|
||||
// 删除数据库记录
|
||||
await deleteImage(imageId);
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Failed to delete image:', error);
|
||||
return NextResponse.json({ error: error.message || 'Failed to delete image' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getProjectPath } from '@/lib/db/base';
|
||||
import { importImagesFromDirectories } from '@/lib/services/images';
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import AdmZip from 'adm-zip';
|
||||
|
||||
// 压缩包解压并导入图片
|
||||
export async function POST(request, { params }) {
|
||||
let tempZipPath = null;
|
||||
let tempExtractDir = null;
|
||||
|
||||
try {
|
||||
const { projectId } = params;
|
||||
const formData = await request.formData();
|
||||
const zipFile = formData.get('file');
|
||||
|
||||
if (!zipFile) {
|
||||
return NextResponse.json({ error: '请选择压缩包文件' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!zipFile.name.toLowerCase().endsWith('.zip')) {
|
||||
return NextResponse.json({ error: '只支持 ZIP 格式的压缩包' }, { status: 400 });
|
||||
}
|
||||
|
||||
const projectPath = await getProjectPath(projectId);
|
||||
const tempDir = path.join(projectPath, 'temp');
|
||||
await fs.mkdir(tempDir, { recursive: true });
|
||||
|
||||
// 1. 保存压缩包到临时目录
|
||||
tempZipPath = path.join(tempDir, `temp_${Date.now()}_${zipFile.name}`);
|
||||
const zipBuffer = Buffer.from(await zipFile.arrayBuffer());
|
||||
await fs.writeFile(tempZipPath, zipBuffer);
|
||||
|
||||
// 2. 创建临时解压目录
|
||||
tempExtractDir = path.join(tempDir, `zip_extract_${Date.now()}`);
|
||||
await fs.mkdir(tempExtractDir, { recursive: true });
|
||||
|
||||
// 3. 使用 adm-zip 解压文件
|
||||
console.log('开始解压压缩包...');
|
||||
const zip = new AdmZip(tempZipPath);
|
||||
const zipEntries = zip.getEntries();
|
||||
|
||||
// 支持的图片扩展名
|
||||
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg'];
|
||||
let extractedCount = 0;
|
||||
|
||||
// 遍历压缩包中的所有文件
|
||||
for (const entry of zipEntries) {
|
||||
// 跳过目录和隐藏文件
|
||||
if (
|
||||
entry.isDirectory ||
|
||||
entry.entryName.startsWith('__MACOSX') ||
|
||||
path.basename(entry.entryName).startsWith('.')
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const ext = path.extname(entry.entryName).toLowerCase();
|
||||
if (imageExtensions.includes(ext)) {
|
||||
// 提取文件名(不包含路径)
|
||||
const fileName = path.basename(entry.entryName);
|
||||
const targetPath = path.join(tempExtractDir, fileName);
|
||||
|
||||
// 解压文件
|
||||
zip.extractEntryTo(entry, tempExtractDir, false, true, false, fileName);
|
||||
extractedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`压缩包解压完成,提取图片数量: ${extractedCount}`);
|
||||
|
||||
if (extractedCount === 0) {
|
||||
throw new Error('压缩包中没有找到支持的图片文件');
|
||||
}
|
||||
|
||||
// 4. 调用服务层导入图片
|
||||
const importResult = await importImagesFromDirectories(projectId, [tempExtractDir]);
|
||||
|
||||
// 5. 清理临时文件
|
||||
try {
|
||||
if (tempZipPath) {
|
||||
await fs.unlink(tempZipPath);
|
||||
}
|
||||
if (tempExtractDir) {
|
||||
const tempImages = await fs.readdir(tempExtractDir);
|
||||
for (const img of tempImages) {
|
||||
await fs.unlink(path.join(tempExtractDir, img));
|
||||
}
|
||||
await fs.rmdir(tempExtractDir);
|
||||
}
|
||||
const tempDirContents = await fs.readdir(tempDir);
|
||||
if (tempDirContents.length === 0) {
|
||||
await fs.rmdir(tempDir);
|
||||
}
|
||||
} catch (cleanupErr) {
|
||||
console.warn('清理临时文件失败:', cleanupErr);
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
count: importResult.count,
|
||||
images: importResult.images,
|
||||
zipName: zipFile.name
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to import ZIP:', error);
|
||||
|
||||
// 清理临时文件
|
||||
try {
|
||||
if (tempZipPath) {
|
||||
await fs.unlink(tempZipPath).catch(() => {});
|
||||
}
|
||||
if (tempExtractDir) {
|
||||
const tempImages = await fs.readdir(tempExtractDir).catch(() => []);
|
||||
for (const img of tempImages) {
|
||||
await fs.unlink(path.join(tempExtractDir, img)).catch(() => {});
|
||||
}
|
||||
await fs.rmdir(tempExtractDir).catch(() => {});
|
||||
}
|
||||
} catch (cleanupErr) {
|
||||
console.warn('清理临时文件失败:', cleanupErr);
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: error.message || 'Failed to import ZIP' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user