- aggiornamento di tante cose...
- generazione Volantini - pagina RIS
This commit is contained in:
398
src/controllers/assetController.js
Normal file
398
src/controllers/assetController.js
Normal file
@@ -0,0 +1,398 @@
|
||||
const Asset = require('../models/Asset');
|
||||
const imageGenerator = require('../services/imageGenerator');
|
||||
const sharp = require('sharp');
|
||||
const path = require('path');
|
||||
const fs = require('fs').promises;
|
||||
|
||||
const UPLOAD_DIR = process.env.UPLOAD_DIR || './uploads';
|
||||
|
||||
const assetController = {
|
||||
// POST /assets/upload
|
||||
async upload(req, res) {
|
||||
try {
|
||||
if (!req.file) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Nessun file caricato'
|
||||
});
|
||||
}
|
||||
|
||||
const { category = 'other', tags, description, isReusable = true } = req.body;
|
||||
const file = req.file;
|
||||
|
||||
// Ottieni dimensioni immagine
|
||||
let dimensions = {};
|
||||
try {
|
||||
const metadata = await sharp(file.path).metadata();
|
||||
dimensions = { width: metadata.width, height: metadata.height };
|
||||
} catch (e) {
|
||||
console.warn('Cannot read image dimensions');
|
||||
}
|
||||
|
||||
// Genera thumbnail
|
||||
const thumbDir = path.join(UPLOAD_DIR, 'thumbs');
|
||||
await fs.mkdir(thumbDir, { recursive: true });
|
||||
const thumbName = `thumb_${file.filename}`;
|
||||
const thumbPath = path.join(thumbDir, thumbName);
|
||||
|
||||
try {
|
||||
await sharp(file.path)
|
||||
.resize(300, 300, { fit: 'cover' })
|
||||
.jpeg({ quality: 80 })
|
||||
.toFile(thumbPath);
|
||||
} catch (e) {
|
||||
console.warn('Cannot create thumbnail');
|
||||
}
|
||||
|
||||
const asset = new Asset({
|
||||
type: 'image',
|
||||
category,
|
||||
sourceType: 'upload',
|
||||
file: {
|
||||
path: file.path,
|
||||
url: `/uploads/${file.filename}`,
|
||||
thumbnailPath: thumbPath,
|
||||
thumbnailUrl: `/uploads/thumbs/${thumbName}`,
|
||||
originalName: file.originalname,
|
||||
mimeType: file.mimetype,
|
||||
size: file.size,
|
||||
dimensions
|
||||
},
|
||||
metadata: {
|
||||
userId: req.user._id,
|
||||
tags: tags ? tags.split(',').map(t => t.trim()) : [],
|
||||
description,
|
||||
isReusable: isReusable === 'true' || isReusable === true
|
||||
}
|
||||
});
|
||||
|
||||
await asset.save();
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: asset
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// POST /assets/upload-multiple
|
||||
async uploadMultiple(req, res) {
|
||||
try {
|
||||
if (!req.files || req.files.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Nessun file caricato'
|
||||
});
|
||||
}
|
||||
|
||||
const { category = 'other' } = req.body;
|
||||
const assets = [];
|
||||
|
||||
for (const file of req.files) {
|
||||
let dimensions = {};
|
||||
try {
|
||||
const metadata = await sharp(file.path).metadata();
|
||||
dimensions = { width: metadata.width, height: metadata.height };
|
||||
} catch (e) {}
|
||||
|
||||
const asset = new Asset({
|
||||
type: 'image',
|
||||
category,
|
||||
sourceType: 'upload',
|
||||
file: {
|
||||
path: file.path,
|
||||
url: `/uploads/${file.filename}`,
|
||||
originalName: file.originalname,
|
||||
mimeType: file.mimetype,
|
||||
size: file.size,
|
||||
dimensions
|
||||
},
|
||||
metadata: {
|
||||
userId: req.user._id
|
||||
}
|
||||
});
|
||||
|
||||
await asset.save();
|
||||
assets.push(asset);
|
||||
}
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: assets
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// POST /assets/generate-ai
|
||||
async generateAi(req, res) {
|
||||
try {
|
||||
const {
|
||||
prompt,
|
||||
negativePrompt,
|
||||
provider = 'hf',
|
||||
category = 'other',
|
||||
aspectRatio = '9:16',
|
||||
model,
|
||||
seed,
|
||||
steps,
|
||||
cfg
|
||||
} = req.body;
|
||||
|
||||
if (!prompt) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Prompt richiesto'
|
||||
});
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
const imageUrl = await imageGenerator.generate(provider, prompt, {
|
||||
negativePrompt,
|
||||
aspectRatio,
|
||||
model,
|
||||
seed,
|
||||
steps,
|
||||
cfg
|
||||
});
|
||||
const generationTime = Date.now() - startTime;
|
||||
|
||||
// Salva file
|
||||
const fileName = `ai_${Date.now()}_${Math.random().toString(36).substr(2, 9)}.jpg`;
|
||||
const filePath = path.join(UPLOAD_DIR, 'ai-generated', fileName);
|
||||
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
||||
|
||||
let fileSize = 0;
|
||||
let dimensions = {};
|
||||
|
||||
if (imageUrl.startsWith('data:')) {
|
||||
const base64Data = imageUrl.replace(/^data:image\/\w+;base64,/, '');
|
||||
const buffer = Buffer.from(base64Data, 'base64');
|
||||
await fs.writeFile(filePath, buffer);
|
||||
fileSize = buffer.length;
|
||||
|
||||
const metadata = await sharp(buffer).metadata();
|
||||
dimensions = { width: metadata.width, height: metadata.height };
|
||||
} else {
|
||||
const fetch = require('node-fetch');
|
||||
const response = await fetch(imageUrl);
|
||||
const buffer = await response.buffer();
|
||||
await fs.writeFile(filePath, buffer);
|
||||
fileSize = buffer.length;
|
||||
|
||||
const metadata = await sharp(buffer).metadata();
|
||||
dimensions = { width: metadata.width, height: metadata.height };
|
||||
}
|
||||
|
||||
const asset = new Asset({
|
||||
type: 'image',
|
||||
category,
|
||||
sourceType: 'ai',
|
||||
file: {
|
||||
path: filePath,
|
||||
url: `/uploads/ai-generated/${fileName}`,
|
||||
mimeType: 'image/jpeg',
|
||||
size: fileSize,
|
||||
dimensions
|
||||
},
|
||||
aiGeneration: {
|
||||
prompt,
|
||||
negativePrompt,
|
||||
provider,
|
||||
model,
|
||||
seed,
|
||||
steps,
|
||||
cfg,
|
||||
requestedSize: aspectRatio,
|
||||
generationTime
|
||||
},
|
||||
metadata: {
|
||||
userId: req.user._id,
|
||||
isReusable: true
|
||||
}
|
||||
});
|
||||
|
||||
await asset.save();
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: asset
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// GET /assets
|
||||
async list(req, res) {
|
||||
try {
|
||||
const {
|
||||
category,
|
||||
sourceType,
|
||||
page = 1,
|
||||
limit = 50
|
||||
} = req.query;
|
||||
|
||||
const query = {
|
||||
'metadata.userId': req.user._id,
|
||||
status: 'ready'
|
||||
};
|
||||
|
||||
if (category) query.category = category;
|
||||
if (sourceType) query.sourceType = sourceType;
|
||||
|
||||
const [assets, total] = await Promise.all([
|
||||
Asset.find(query)
|
||||
.sort({ createdAt: -1 })
|
||||
.skip((page - 1) * limit)
|
||||
.limit(parseInt(limit)),
|
||||
Asset.countDocuments(query)
|
||||
]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: assets,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
total,
|
||||
pages: Math.ceil(total / limit)
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// GET /assets/:id
|
||||
async getById(req, res) {
|
||||
try {
|
||||
const asset = await Asset.findById(req.params.id);
|
||||
|
||||
if (!asset) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Asset non trovato'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: asset
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// GET /assets/:id/file
|
||||
async getFile(req, res) {
|
||||
try {
|
||||
const asset = await Asset.findById(req.params.id);
|
||||
|
||||
if (!asset || !asset.file?.path) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'File non trovato'
|
||||
});
|
||||
}
|
||||
|
||||
res.sendFile(path.resolve(asset.file.path));
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// GET /assets/:id/thumbnail
|
||||
async getThumbnail(req, res) {
|
||||
try {
|
||||
const asset = await Asset.findById(req.params.id);
|
||||
|
||||
if (!asset) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Asset non trovato'
|
||||
});
|
||||
}
|
||||
|
||||
const thumbPath = asset.file?.thumbnailPath || asset.file?.path;
|
||||
if (!thumbPath) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Thumbnail non disponibile'
|
||||
});
|
||||
}
|
||||
|
||||
res.sendFile(path.resolve(thumbPath));
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// DELETE /assets/:id
|
||||
async delete(req, res) {
|
||||
try {
|
||||
const asset = await Asset.findById(req.params.id);
|
||||
|
||||
if (!asset) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Asset non trovato'
|
||||
});
|
||||
}
|
||||
|
||||
if (asset.metadata.userId.toString() !== req.user._id.toString()) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
error: 'Non autorizzato'
|
||||
});
|
||||
}
|
||||
|
||||
// Elimina file
|
||||
try {
|
||||
if (asset.file?.path) await fs.unlink(asset.file.path);
|
||||
if (asset.file?.thumbnailPath) await fs.unlink(asset.file.thumbnailPath);
|
||||
} catch (e) {
|
||||
console.warn('File deletion warning:', e.message);
|
||||
}
|
||||
|
||||
await Asset.deleteOne({ _id: asset._id });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Asset eliminato'
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = assetController;
|
||||
647
src/controllers/posterController.js
Normal file
647
src/controllers/posterController.js
Normal file
@@ -0,0 +1,647 @@
|
||||
const Poster = require('../models/Poster');
|
||||
const Template = require('../models/Template');
|
||||
const Asset = require('../models/Asset');
|
||||
const posterRenderer = require('../services/posterRenderer');
|
||||
const imageGenerator = require('../services/imageGenerator');
|
||||
const path = require('path');
|
||||
const fs = require('fs').promises;
|
||||
|
||||
const UPLOAD_DIR = process.env.UPLOAD_DIR || './uploads';
|
||||
|
||||
const posterController = {
|
||||
// POST /posters
|
||||
async create(req, res) {
|
||||
try {
|
||||
const {
|
||||
templateId,
|
||||
name,
|
||||
description,
|
||||
content,
|
||||
assets,
|
||||
layerOverrides,
|
||||
autoRender = false
|
||||
} = req.body;
|
||||
|
||||
// Carica template
|
||||
const template = await Template.findById(templateId);
|
||||
if (!template) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Template non trovato'
|
||||
});
|
||||
}
|
||||
|
||||
// Valida contenuti richiesti
|
||||
const requiredLayers = template.layers.filter(l => l.required);
|
||||
for (const layer of requiredLayers) {
|
||||
if (layer.type === 'title' && !content?.title) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: `Campo richiesto: ${layer.type}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const poster = new Poster({
|
||||
templateId,
|
||||
templateSnapshot: template.toObject(), // Snapshot per retrocompatibilità
|
||||
name: name || content?.title || 'Nuova Locandina',
|
||||
description,
|
||||
status: 'draft',
|
||||
content: content || {},
|
||||
assets: assets || {},
|
||||
layerOverrides: layerOverrides || {},
|
||||
renderEngineVersion: '1.0.0',
|
||||
metadata: {
|
||||
userId: req.user._id
|
||||
}
|
||||
});
|
||||
|
||||
await poster.save();
|
||||
|
||||
// Incrementa uso template
|
||||
await template.incrementUsage();
|
||||
|
||||
// Auto-render se richiesto
|
||||
if (autoRender) {
|
||||
await posterController._renderPoster(poster);
|
||||
await poster.save();
|
||||
}
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: poster
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Poster create error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// GET /posters
|
||||
async list(req, res) {
|
||||
try {
|
||||
const {
|
||||
status,
|
||||
templateId,
|
||||
search,
|
||||
page = 1,
|
||||
limit = 20,
|
||||
sortBy = 'createdAt',
|
||||
sortOrder = 'desc'
|
||||
} = req.query;
|
||||
|
||||
const query = { 'metadata.userId': req.user._id };
|
||||
|
||||
if (status) query.status = status;
|
||||
if (templateId) query.templateId = templateId;
|
||||
if (search) query.$text = { $search: search };
|
||||
|
||||
const sort = { [sortBy]: sortOrder === 'asc' ? 1 : -1 };
|
||||
|
||||
const [posters, total] = await Promise.all([
|
||||
Poster.find(query)
|
||||
.populate('templateId', 'name templateType thumbnailUrl')
|
||||
.sort(sort)
|
||||
.skip((page - 1) * limit)
|
||||
.limit(parseInt(limit))
|
||||
.select('-templateSnapshot -history'),
|
||||
Poster.countDocuments(query)
|
||||
]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: posters,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
total,
|
||||
pages: Math.ceil(total / limit)
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// GET /posters/favorites
|
||||
async listFavorites(req, res) {
|
||||
try {
|
||||
const posters = await Poster.findFavorites(req.user._id);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: posters
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// GET /posters/recent
|
||||
async listRecent(req, res) {
|
||||
try {
|
||||
const { limit = 10 } = req.query;
|
||||
const posters = await Poster.findRecent(req.user._id, parseInt(limit));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: posters
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// GET /posters/:id
|
||||
async getById(req, res) {
|
||||
try {
|
||||
const poster = await Poster.findById(req.params.id)
|
||||
.populate('templateId')
|
||||
.populate('assets.backgroundImage.assetId')
|
||||
.populate('assets.mainImage.assetId')
|
||||
.populate('assets.logos.assetId');
|
||||
|
||||
if (!poster) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Poster non trovato'
|
||||
});
|
||||
}
|
||||
|
||||
// Check ownership
|
||||
if (poster.metadata.userId.toString() !== req.user._id.toString()) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
error: 'Accesso negato'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: poster
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// PUT /posters/:id
|
||||
async update(req, res) {
|
||||
try {
|
||||
const poster = await Poster.findById(req.params.id);
|
||||
|
||||
if (!poster) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Poster non trovato'
|
||||
});
|
||||
}
|
||||
|
||||
if (poster.metadata.userId.toString() !== req.user._id.toString()) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
error: 'Non autorizzato'
|
||||
});
|
||||
}
|
||||
|
||||
const updateFields = [
|
||||
'name', 'description', 'content', 'assets', 'layerOverrides'
|
||||
];
|
||||
|
||||
updateFields.forEach(field => {
|
||||
if (req.body[field] !== undefined) {
|
||||
poster[field] = req.body[field];
|
||||
}
|
||||
});
|
||||
|
||||
// Invalida render precedente se contenuto modificato
|
||||
if (req.body.content || req.body.assets || req.body.layerOverrides) {
|
||||
poster.status = 'draft';
|
||||
poster.addHistory('updated', { fields: Object.keys(req.body) });
|
||||
}
|
||||
|
||||
await poster.save();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: poster
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// DELETE /posters/:id
|
||||
async delete(req, res) {
|
||||
try {
|
||||
const poster = await Poster.findById(req.params.id);
|
||||
|
||||
if (!poster) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Poster non trovato'
|
||||
});
|
||||
}
|
||||
|
||||
if (poster.metadata.userId.toString() !== req.user._id.toString()) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
error: 'Non autorizzato'
|
||||
});
|
||||
}
|
||||
|
||||
// Elimina file renderizzati
|
||||
if (poster.renderOutput) {
|
||||
const filesToDelete = [
|
||||
poster.renderOutput.png?.path,
|
||||
poster.renderOutput.jpg?.path,
|
||||
poster.renderOutput.webp?.path
|
||||
].filter(Boolean);
|
||||
|
||||
for (const filePath of filesToDelete) {
|
||||
try {
|
||||
await fs.unlink(filePath);
|
||||
} catch (e) {
|
||||
console.warn('File not found:', filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await Poster.deleteOne({ _id: poster._id });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Poster eliminato'
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// POST /posters/:id/render
|
||||
async render(req, res) {
|
||||
try {
|
||||
const poster = await Poster.findById(req.params.id)
|
||||
.populate('templateId');
|
||||
|
||||
if (!poster) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Poster non trovato'
|
||||
});
|
||||
}
|
||||
|
||||
if (poster.metadata.userId.toString() !== req.user._id.toString()) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
error: 'Non autorizzato'
|
||||
});
|
||||
}
|
||||
|
||||
poster.status = 'processing';
|
||||
await poster.save();
|
||||
|
||||
try {
|
||||
await posterController._renderPoster(poster);
|
||||
await poster.save();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
status: poster.status,
|
||||
renderOutput: poster.renderOutput
|
||||
}
|
||||
});
|
||||
} catch (renderError) {
|
||||
poster.setError(renderError.message);
|
||||
await poster.save();
|
||||
throw renderError;
|
||||
}
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// POST /posters/:id/regenerate-ai
|
||||
async regenerateAi(req, res) {
|
||||
try {
|
||||
const { assetType, prompt, provider = 'hf' } = req.body;
|
||||
const poster = await Poster.findById(req.params.id);
|
||||
|
||||
if (!poster) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Poster non trovato'
|
||||
});
|
||||
}
|
||||
|
||||
if (poster.metadata.userId.toString() !== req.user._id.toString()) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
error: 'Non autorizzato'
|
||||
});
|
||||
}
|
||||
|
||||
// Genera nuova immagine AI
|
||||
const startTime = Date.now();
|
||||
const imageUrl = await imageGenerator.generate(provider, prompt);
|
||||
const generationTime = Date.now() - startTime;
|
||||
|
||||
// Salva su filesystem
|
||||
const fileName = `${poster._id}_${assetType}_${Date.now()}.jpg`;
|
||||
const filePath = path.join(UPLOAD_DIR, 'ai-generated', fileName);
|
||||
|
||||
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
||||
|
||||
// Se è base64, converti
|
||||
let savedPath;
|
||||
if (imageUrl.startsWith('data:')) {
|
||||
const base64Data = imageUrl.replace(/^data:image\/\w+;base64,/, '');
|
||||
await fs.writeFile(filePath, base64Data, 'base64');
|
||||
savedPath = filePath;
|
||||
} else {
|
||||
// Se è URL, scarica
|
||||
const fetch = require('node-fetch');
|
||||
const response = await fetch(imageUrl);
|
||||
const buffer = await response.buffer();
|
||||
await fs.writeFile(filePath, buffer);
|
||||
savedPath = filePath;
|
||||
}
|
||||
|
||||
// Aggiorna asset nel poster
|
||||
const assetData = {
|
||||
sourceType: 'ai',
|
||||
url: `/uploads/ai-generated/${fileName}`,
|
||||
mimeType: 'image/jpeg',
|
||||
aiParams: {
|
||||
prompt,
|
||||
provider,
|
||||
generatedAt: new Date()
|
||||
}
|
||||
};
|
||||
|
||||
if (assetType === 'backgroundImage') {
|
||||
poster.assets.backgroundImage = assetData;
|
||||
poster.addHistory('ai_background_generated', { provider, duration: generationTime });
|
||||
} else if (assetType === 'mainImage') {
|
||||
poster.assets.mainImage = assetData;
|
||||
poster.addHistory('ai_main_generated', { provider, duration: generationTime });
|
||||
}
|
||||
|
||||
poster.status = 'draft';
|
||||
await poster.save();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
assetType,
|
||||
asset: assetData
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// GET /posters/:id/download/:format
|
||||
async download(req, res) {
|
||||
try {
|
||||
const { format } = req.params;
|
||||
const poster = await Poster.findById(req.params.id);
|
||||
|
||||
if (!poster) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Poster non trovato'
|
||||
});
|
||||
}
|
||||
|
||||
const outputFile = poster.renderOutput?.[format];
|
||||
if (!outputFile?.path) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: `Formato ${format} non disponibile`
|
||||
});
|
||||
}
|
||||
|
||||
// Incrementa download count
|
||||
await poster.incrementDownload();
|
||||
|
||||
const fileName = `${poster.name.replace(/[^a-z0-9]/gi, '_')}_poster.${format}`;
|
||||
|
||||
res.download(outputFile.path, fileName);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// POST /posters/:id/favorite
|
||||
async toggleFavorite(req, res) {
|
||||
try {
|
||||
const poster = await Poster.findById(req.params.id);
|
||||
|
||||
if (!poster) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Poster non trovato'
|
||||
});
|
||||
}
|
||||
|
||||
if (poster.metadata.userId.toString() !== req.user._id.toString()) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
error: 'Non autorizzato'
|
||||
});
|
||||
}
|
||||
|
||||
await poster.toggleFavorite();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
isFavorite: poster.metadata.isFavorite
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// POST /posters/quick-generate (compatibile con la tua bozza)
|
||||
async quickGenerate(req, res) {
|
||||
try {
|
||||
const {
|
||||
templateId,
|
||||
titolo,
|
||||
descrizione,
|
||||
data,
|
||||
ora,
|
||||
luogo,
|
||||
contatti,
|
||||
fotoDescrizione,
|
||||
stile,
|
||||
provider = 'hf',
|
||||
aspectRatio = '9:16'
|
||||
} = req.body;
|
||||
|
||||
// Validazione base
|
||||
if (!titolo || !data || !luogo) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Compila titolo, data e luogo'
|
||||
});
|
||||
}
|
||||
|
||||
// Usa template default o quello specificato
|
||||
let template;
|
||||
if (templateId) {
|
||||
template = await Template.findById(templateId);
|
||||
} else {
|
||||
// Template default per quick-generate
|
||||
template = await Template.findOne({
|
||||
templateType: 'quick-generate',
|
||||
isActive: true
|
||||
});
|
||||
}
|
||||
|
||||
// Genera prompt per AI background
|
||||
const aiPrompt = `Vertical event poster background, ${stile || 'modern style, vivid colors'}. Subject: ${fotoDescrizione || 'abstract artistic shapes'}. Composition: Central empty space suitable for text overlay. NO TEXT, NO LETTERS, clean illustration, high quality, 4k.`;
|
||||
|
||||
// Genera immagine AI
|
||||
const startTime = Date.now();
|
||||
const rawImageUrl = await imageGenerator.generate(provider, aiPrompt);
|
||||
const generationTime = Date.now() - startTime;
|
||||
|
||||
// Salva asset generato
|
||||
const fileName = `quick_${Date.now()}.jpg`;
|
||||
const filePath = path.join(UPLOAD_DIR, 'ai-generated', fileName);
|
||||
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
||||
|
||||
if (rawImageUrl.startsWith('data:')) {
|
||||
const base64Data = rawImageUrl.replace(/^data:image\/\w+;base64,/, '');
|
||||
await fs.writeFile(filePath, base64Data, 'base64');
|
||||
}
|
||||
|
||||
// Crea poster
|
||||
const poster = new Poster({
|
||||
templateId: template?._id,
|
||||
name: titolo,
|
||||
status: 'processing',
|
||||
content: {
|
||||
title: titolo,
|
||||
subtitle: descrizione,
|
||||
eventDate: data,
|
||||
eventTime: ora,
|
||||
location: luogo,
|
||||
contacts: contatti
|
||||
},
|
||||
assets: {
|
||||
backgroundImage: {
|
||||
sourceType: 'ai',
|
||||
url: `/uploads/ai-generated/${fileName}`,
|
||||
mimeType: 'image/jpeg',
|
||||
aiParams: {
|
||||
prompt: aiPrompt,
|
||||
provider,
|
||||
generatedAt: new Date()
|
||||
}
|
||||
}
|
||||
},
|
||||
originalPrompt: aiPrompt,
|
||||
styleUsed: stile,
|
||||
aspectRatio,
|
||||
provider,
|
||||
metadata: {
|
||||
userId: req.user._id
|
||||
}
|
||||
});
|
||||
|
||||
poster.addHistory('ai_background_generated', { provider, duration: generationTime });
|
||||
|
||||
// Render con testi sovrapposti
|
||||
await posterController._renderPoster(poster, { useQuickRender: true });
|
||||
await poster.save();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
posterId: poster._id,
|
||||
imageUrl: poster.renderOutput?.png?.url || rawImageUrl,
|
||||
status: poster.status
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Quick generate error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// Helper interno: renderizza poster
|
||||
async _renderPoster(poster, options = {}) {
|
||||
const template = poster.templateId || poster.templateSnapshot;
|
||||
|
||||
const result = await posterRenderer.render({
|
||||
template,
|
||||
content: poster.content,
|
||||
assets: poster.assets,
|
||||
layerOverrides: Object.fromEntries(poster.layerOverrides || new Map()),
|
||||
outputDir: path.join(UPLOAD_DIR, 'posters', 'final'),
|
||||
posterId: poster._id.toString()
|
||||
});
|
||||
|
||||
poster.setRenderOutput({
|
||||
png: {
|
||||
path: result.pngPath,
|
||||
url: `/uploads/posters/final/${path.basename(result.pngPath)}`,
|
||||
size: result.pngSize
|
||||
},
|
||||
jpg: {
|
||||
path: result.jpgPath,
|
||||
url: `/uploads/posters/final/${path.basename(result.jpgPath)}`,
|
||||
size: result.jpgSize,
|
||||
quality: 95
|
||||
},
|
||||
dimensions: result.dimensions,
|
||||
duration: result.duration
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = posterController;
|
||||
383
src/controllers/templateController.js
Normal file
383
src/controllers/templateController.js
Normal file
@@ -0,0 +1,383 @@
|
||||
const Template = require('../models/Template');
|
||||
|
||||
// Presets formati standard
|
||||
const FORMAT_PRESETS = {
|
||||
'A4': { width: 2480, height: 3508, dpi: 300 },
|
||||
'A4-landscape': { width: 3508, height: 2480, dpi: 300 },
|
||||
'A3': { width: 3508, height: 4961, dpi: 300 },
|
||||
'A3-landscape': { width: 4961, height: 3508, dpi: 300 },
|
||||
'instagram-post': { width: 1080, height: 1080, dpi: 72 },
|
||||
'instagram-story': { width: 1080, height: 1920, dpi: 72 },
|
||||
'instagram-portrait': { width: 1080, height: 1350, dpi: 72 },
|
||||
'facebook-post': { width: 1200, height: 630, dpi: 72 },
|
||||
'facebook-event': { width: 1920, height: 1080, dpi: 72 },
|
||||
'twitter-post': { width: 1200, height: 675, dpi: 72 },
|
||||
'poster-24x36': { width: 7200, height: 10800, dpi: 300 },
|
||||
'flyer-5x7': { width: 1500, height: 2100, dpi: 300 }
|
||||
};
|
||||
|
||||
const templateController = {
|
||||
// POST /templates
|
||||
async create(req, res) {
|
||||
try {
|
||||
const {
|
||||
name,
|
||||
templateType,
|
||||
description,
|
||||
format,
|
||||
safeArea,
|
||||
backgroundColor,
|
||||
layers,
|
||||
logoSlots,
|
||||
palette,
|
||||
typography,
|
||||
defaultAiPromptHints,
|
||||
metadata
|
||||
} = req.body;
|
||||
|
||||
// Applica preset se specificato
|
||||
let finalFormat = format;
|
||||
if (format?.preset && FORMAT_PRESETS[format.preset]) {
|
||||
finalFormat = {
|
||||
...FORMAT_PRESETS[format.preset],
|
||||
preset: format.preset,
|
||||
unit: 'px'
|
||||
};
|
||||
}
|
||||
|
||||
// Valida layers
|
||||
if (!layers || !Array.isArray(layers) || layers.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Almeno un layer è richiesto'
|
||||
});
|
||||
}
|
||||
|
||||
// Assicura ID unici per layer
|
||||
const layersWithIds = layers.map((layer, idx) => ({
|
||||
...layer,
|
||||
id: layer.id || `layer_${layer.type}_${idx}`
|
||||
}));
|
||||
|
||||
const template = new Template({
|
||||
name,
|
||||
templateType,
|
||||
description,
|
||||
format: finalFormat,
|
||||
safeArea: safeArea || {},
|
||||
backgroundColor: backgroundColor || '#1a1a2e',
|
||||
layers: layersWithIds,
|
||||
logoSlots: logoSlots || { enabled: false, slots: [] },
|
||||
palette: palette || {},
|
||||
typography: typography || {},
|
||||
defaultAiPromptHints: defaultAiPromptHints || {},
|
||||
metadata: {
|
||||
...metadata,
|
||||
author: req.user?.name || 'System'
|
||||
},
|
||||
userId: req.user?._id
|
||||
});
|
||||
|
||||
await template.save();
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: template
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Template create error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// GET /templates
|
||||
async list(req, res) {
|
||||
try {
|
||||
const {
|
||||
type,
|
||||
search,
|
||||
tags,
|
||||
page = 1,
|
||||
limit = 20,
|
||||
sortBy = 'createdAt',
|
||||
sortOrder = 'desc'
|
||||
} = req.query;
|
||||
|
||||
const query = { isActive: true };
|
||||
|
||||
if (type) {
|
||||
query.templateType = type;
|
||||
}
|
||||
|
||||
if (tags) {
|
||||
const tagArray = tags.split(',').map(t => t.trim());
|
||||
query['metadata.tags'] = { $in: tagArray };
|
||||
}
|
||||
|
||||
if (search) {
|
||||
query.$text = { $search: search };
|
||||
}
|
||||
|
||||
// Se utente autenticato, mostra anche i suoi privati
|
||||
if (req.user) {
|
||||
query.$or = [
|
||||
{ 'metadata.isPublic': true },
|
||||
{ userId: req.user._id }
|
||||
];
|
||||
} else {
|
||||
query['metadata.isPublic'] = true;
|
||||
}
|
||||
|
||||
const sort = { [sortBy]: sortOrder === 'asc' ? 1 : -1 };
|
||||
|
||||
const [templates, total] = await Promise.all([
|
||||
Template.find(query)
|
||||
.sort(sort)
|
||||
.skip((page - 1) * limit)
|
||||
.limit(parseInt(limit))
|
||||
.select('-layers -logoSlots'), // Escludi dati pesanti per list
|
||||
Template.countDocuments(query)
|
||||
]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: templates,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
total,
|
||||
pages: Math.ceil(total / limit)
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Template list error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// GET /templates/types
|
||||
async getTypes(req, res) {
|
||||
try {
|
||||
const types = await Template.distinct('templateType', { isActive: true });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: types.sort()
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// GET /templates/presets
|
||||
async getFormatPresets(req, res) {
|
||||
res.json({
|
||||
success: true,
|
||||
data: FORMAT_PRESETS
|
||||
});
|
||||
},
|
||||
|
||||
// GET /templates/:id
|
||||
async getById(req, res) {
|
||||
try {
|
||||
const template = await Template.findById(req.params.id);
|
||||
|
||||
if (!template) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Template non trovato'
|
||||
});
|
||||
}
|
||||
|
||||
// Check accesso
|
||||
if (!template.metadata.isPublic &&
|
||||
(!req.user || template.userId?.toString() !== req.user._id.toString())) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
error: 'Accesso negato'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: template
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// PUT /templates/:id
|
||||
async update(req, res) {
|
||||
try {
|
||||
const template = await Template.findById(req.params.id);
|
||||
|
||||
if (!template) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Template non trovato'
|
||||
});
|
||||
}
|
||||
|
||||
// Check ownership
|
||||
if (template.userId?.toString() !== req.user._id.toString()) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
error: 'Non autorizzato a modificare questo template'
|
||||
});
|
||||
}
|
||||
|
||||
const updateFields = [
|
||||
'name', 'description', 'templateType', 'format', 'safeArea',
|
||||
'backgroundColor', 'layers', 'logoSlots', 'palette',
|
||||
'typography', 'defaultAiPromptHints', 'metadata', 'isActive'
|
||||
];
|
||||
|
||||
updateFields.forEach(field => {
|
||||
if (req.body[field] !== undefined) {
|
||||
template[field] = req.body[field];
|
||||
}
|
||||
});
|
||||
|
||||
// Incrementa versione
|
||||
if (template.metadata) {
|
||||
const version = template.metadata.version || '1.0.0';
|
||||
const parts = version.split('.').map(Number);
|
||||
parts[2]++;
|
||||
template.metadata.version = parts.join('.');
|
||||
}
|
||||
|
||||
await template.save();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: template
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// DELETE /templates/:id
|
||||
async delete(req, res) {
|
||||
try {
|
||||
const template = await Template.findById(req.params.id);
|
||||
|
||||
if (!template) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Template non trovato'
|
||||
});
|
||||
}
|
||||
|
||||
if (template.userId?.toString() !== req.user._id.toString()) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
error: 'Non autorizzato'
|
||||
});
|
||||
}
|
||||
|
||||
// Soft delete
|
||||
template.isActive = false;
|
||||
await template.save();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Template eliminato'
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// POST /templates/:id/duplicate
|
||||
async duplicate(req, res) {
|
||||
try {
|
||||
const original = await Template.findById(req.params.id);
|
||||
|
||||
if (!original) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Template non trovato'
|
||||
});
|
||||
}
|
||||
|
||||
const duplicateData = original.toObject();
|
||||
delete duplicateData._id;
|
||||
delete duplicateData.createdAt;
|
||||
delete duplicateData.updatedAt;
|
||||
|
||||
duplicateData.name = `${original.name} (copia)`;
|
||||
duplicateData.userId = req.user._id;
|
||||
duplicateData.metadata = {
|
||||
...duplicateData.metadata,
|
||||
isPublic: false,
|
||||
usageCount: 0,
|
||||
author: req.user?.name || 'System',
|
||||
version: '1.0.0'
|
||||
};
|
||||
|
||||
const duplicate = new Template(duplicateData);
|
||||
await duplicate.save();
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: duplicate
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// GET /templates/:id/preview
|
||||
async getPreview(req, res) {
|
||||
try {
|
||||
const template = await Template.findById(req.params.id)
|
||||
.select('previewUrl thumbnailUrl name');
|
||||
|
||||
if (!template) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Template non trovato'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
previewUrl: template.previewUrl,
|
||||
thumbnailUrl: template.thumbnailUrl,
|
||||
name: template.name
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = templateController;
|
||||
Reference in New Issue
Block a user