photo.js 4.69 KB
/**
 * photo.js — 多模态资产关联 REST 路由
 *
 * POST /api/photo/link               body: { imagePath, personNameOrId }
 * GET  /api/photo/person/:id         → 该联系人所有照片
 * POST /api/photo/scan-dir           body: { dirPath } → 扫描目录中所有图片的人脸
 */

const express = require('express');
const router = express.Router();
const fs = require('fs');
const path = require('path');
const { PhotoService } = require('../services/photo/photo-service');
const { getGraphStore } = require('../db/GraphStore');

// 支持的图片扩展名
const IMAGE_EXTS = new Set(['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.tiff', '.tif']);

// ── POST /api/photo/link ─────────────────────────────────────────────
router.post('/link', async (req, res) => {
    try {
        const { imagePath, personNameOrId } = req.body;
        if (!imagePath || !personNameOrId) {
            return res.status(400).json({ error: 'imagePath 和 personNameOrId 为必填字段' });
        }

        if (!fs.existsSync(imagePath)) {
            return res.status(400).json({ error: `文件不存在:${imagePath}` });
        }

        const svc = new PhotoService(imagePath);
        const result = await svc.linkPhotoToPerson(imagePath, personNameOrId);

        res.json({
            success: true,
            assetId: result.assetId,
            personId: result.personId,
            personName: result.personName,
            exif: result.exif,
        });
    } catch (err) {
        res.status(500).json({ error: err.message });
    }
});

// ── GET /api/photo/person/:id ────────────────────────────────────────
router.get('/person/:id', (req, res) => {
    try {
        const personId = req.params.id;
        const g = getGraphStore();

        // 解析联系人
        let person = g.getNode(personId);
        if (!person || person._type !== 'Person') {
            const results = g.findByName(personId, 'Person');
            person = results[0] || null;
        }
        if (!person) {
            return res.status(404).json({ error: `未找到联系人:${personId}` });
        }

        const { outgoing } = g.getEdges(person.id);
        const photos = [];

        for (const edge of outgoing) {
            if (edge.relation !== 'CONTAINS' && edge.relation !== 'ASSOCIATED') continue;
            const asset = g.getNode(edge.to_id);
            if (asset && asset._type === 'Asset' && asset.type === 'photo') {
                photos.push(asset);
            }
        }

        res.json({
            success: true,
            personId: person.id,
            personName: person.name,
            count: photos.length,
            photos,
        });
    } catch (err) {
        res.status(500).json({ error: err.message });
    }
});

// ── POST /api/photo/scan-dir ─────────────────────────────────────────
router.post('/scan-dir', async (req, res) => {
    try {
        const { dirPath } = req.body;
        if (!dirPath) {
            return res.status(400).json({ error: 'dirPath 为必填字段' });
        }

        if (!fs.existsSync(dirPath)) {
            return res.status(400).json({ error: `目录不存在:${dirPath}` });
        }

        const stat = fs.statSync(dirPath);
        if (!stat.isDirectory()) {
            return res.status(400).json({ error: `${dirPath} 不是目录` });
        }

        // 枚举目录中的图片文件
        const files = fs.readdirSync(dirPath)
            .filter(f => IMAGE_EXTS.has(path.extname(f).toLowerCase()))
            .map(f => path.join(dirPath, f));

        const results = [];

        for (const imgPath of files) {
            try {
                const svc = new PhotoService(imgPath);
                const faces = await svc.detectFaces(imgPath);
                results.push({
                    file: imgPath,
                    fileName: path.basename(imgPath),
                    faceCount: Array.isArray(faces) ? faces.length : 0,
                    faces: Array.isArray(faces) ? faces : [],
                });
            } catch (e) {
                results.push({
                    file: imgPath,
                    fileName: path.basename(imgPath),
                    error: e.message,
                });
            }
        }

        res.json({
            success: true,
            dirPath,
            totalFiles: files.length,
            results,
        });
    } catch (err) {
        res.status(500).json({ error: err.message });
    }
});

module.exports = router;