Upscale / index.html
NathMen12's picture
Update index.html
9734406 verified
Raw
History Blame Contribute Delete
25.8 kB
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Batch AI & GPU Upscaler (Fix Tout)</title>
<!-- ONNX Runtime Web via CDN JSDelivr -->
<script src="https://cdn.jsdelivr.net/npm/onnxruntime-web@1.17.1/dist/ort.min.js"></script>
<style>
:root {
--bg: #0d1117;
--panel: #161b22;
--border: #30363d;
--text: #c9d1d9;
--heading: #f0f6fc;
--primary: #238636;
--primary-hover: #2ea043;
--accent: #58a6ff;
--danger: #f85149;
--warning: #d29922;
}
* { box-sizing: border-box; margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
body { background-color: var(--bg); color: var(--text); padding: 2rem 1rem; display: flex; justify-content: center; min-height: 100vh; }
.container { max-width: 950px; width: 100%; background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 2rem; box-shadow: 0 8px 24px rgba(0,0,0,0.5); }
h1 { color: var(--heading); font-size: 1.5rem; margin-bottom: 0.5rem; display: flex; align-items: center; gap: 8px; }
p.subtitle { color: #8b949e; font-size: 0.9rem; margin-bottom: 1.5rem; }
.status-box {
background: #010409;
border: 1px solid var(--border);
padding: 1.2rem;
border-radius: 6px;
margin-bottom: 1.5rem;
display: flex;
flex-direction: column;
gap: 12px;
}
.btn-group { display: flex; gap: 10px; flex-wrap: wrap; }
.dropzone {
border: 2px dashed var(--border);
border-radius: 6px;
padding: 2.5rem 1rem;
text-align: center;
cursor: pointer;
background: rgba(22, 27, 34, 0.5);
margin-bottom: 1.5rem;
transition: all 0.2s ease;
}
.dropzone:hover, .dropzone.dragover { border-color: var(--accent); background: rgba(88, 166, 255, 0.1); }
.btn {
background: var(--primary);
color: #fff;
border: none;
padding: 10px 18px;
font-size: 0.95rem;
font-weight: 600;
border-radius: 6px;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 6px;
transition: background 0.2s;
}
.btn:hover { background: var(--primary-hover); }
.btn:disabled { background: #21262d; color: #484f58; cursor: not-allowed; }
.btn-blue { background: #1f6feb; }
.btn-blue:hover { background: #388bfd; }
.btn-grey { background: #30363d; }
.btn-grey:hover { background: #484f58; }
.controls { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem; }
.image-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 16px; }
.card { background: #0d1117; border: 1px solid var(--border); border-radius: 6px; overflow: hidden; display: flex; flex-direction: column; }
.preview-box { height: 180px; background: #010409; display: flex; align-items: center; justify-content: center; position: relative; }
.preview-box img { max-width: 100%; max-height: 100%; object-fit: contain; }
.badge { position: absolute; top: 8px; right: 8px; padding: 4px 8px; border-radius: 12px; font-size: 0.75rem; font-weight: bold; }
.status-waiting { background: #21262d; color: #8b949e; }
.status-processing { background: #1f6feb; color: #fff; }
.status-done { background: #238636; color: #fff; }
.status-fallback { background: #d29922; color: #fff; }
.status-error { background: #da3633; color: #fff; }
.card-body { padding: 12px; display: flex; flex-direction: column; gap: 8px; }
.download-btn { background: #21262d; border: 1px solid var(--border); color: var(--accent); text-decoration: none; padding: 8px 12px; border-radius: 4px; font-size: 0.85rem; text-align: center; font-weight: 600; display: block; }
.download-btn:hover { background: #30363d; border-color: var(--accent); }
.info-tag { font-size: 0.8rem; padding: 2px 6px; border-radius: 4px; background: rgba(88, 166, 255, 0.15); color: var(--accent); display: inline-block; width: fit-content; }
</style>
</head>
<body>
<div class="container">
<h1>🚀 Batch AI & GPU Upscaler <span class="info-tag">100% Client-Side</span></h1>
<p class="subtitle">Upscale d'images en haute résolution via Modèle IA ONNX ou Algorithme Shader GPU WebGL local.</p>
<div class="status-box">
<div style="display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:10px;">
<div>
<strong>État du Moteur :</strong>
<span id="modelStatus" style="color:#d29922; font-weight:bold;">Mode GPU WebGL prêt (IA optionnelle)</span>
</div>
<div class="btn-group">
<button id="downloadModelBtn" class="btn btn-blue" onclick="loadAIModelCDN()">📥 Charger Modèle IA CDN</button>
<button class="btn btn-grey" onclick="document.getElementById('onnxFileInput').click()">📂 Charger .onnx Local</button>
</div>
<input type="file" id="onnxFileInput" accept=".onnx" style="display:none;" onchange="loadLocalModelFile(this.files[0])">
</div>
</div>
<div class="dropzone" id="dropzone">
<p style="font-size:1.1rem; font-weight:600; color:var(--heading);">Glissez-déposez vos images ici ou cliquez pour parcourir</p>
<p style="font-size: 0.85rem; color: #8b949e; margin-top: 8px;">PNG, JPG, WebP | Multi-imports illimités (plusieurs fichiers ou ajouts successifs)</p>
<input type="file" id="fileInput" multiple accept="image/*" style="display: none;">
</div>
<div class="controls">
<button id="startBtn" class="btn" onclick="startBatchProcessing()" disabled>🚀 Lancer l'Upscale Batch</button>
<span id="statsText" style="color: #8b949e; font-weight:500;">0 fichier(s) dans la liste</span>
</div>
<div class="image-grid" id="imageGrid"></div>
</div>
<canvas id="glCanvas" style="display:none;"></canvas>
<script>
let session = null;
let queue = [];
let isProcessing = false;
// CDN ultra-fiable pour modèle ONNX ESPCN
const MODEL_URL = "https://raw.githubusercontent.com/onnx/models/main/validated/vision/super_resolution/sub_pixel_cnn_2016/model/super-resolution-10.onnx";
const fileInput = document.getElementById('fileInput');
const dropzone = document.getElementById('dropzone');
const startBtn = document.getElementById('startBtn');
const modelStatus = document.getElementById('modelStatus');
// ----------------------------------------------------
// 1. CHARGEMENT MODÈLE ONNX (CDN ou LOCAL)
// ----------------------------------------------------
async function loadAIModelCDN() {
modelStatus.innerText = "Téléchargement du modèle IA...";
modelStatus.style.color = "#58a6ff";
try {
ort.env.wasm.numThreads = 2;
session = await ort.InferenceSession.create(MODEL_URL, {
executionProviders: ['webgl', 'wasm', 'cpu']
});
modelStatus.innerHTML = "✅ Modèle IA ONNX prêt en mémoire (WebGL/WASM)";
modelStatus.style.color = "#2ea043";
updateControls();
} catch (err) {
console.error("Erreur chargement CDN:", err);
modelStatus.innerHTML = "⚠️ Mode GPU WebGL actif (Chargement IA CDN bloqué par le navigateur)";
modelStatus.style.color = "#d29922";
updateControls();
}
}
async function loadLocalModelFile(file) {
if (!file) return;
modelStatus.innerText = "Chargement du fichier ONNX local...";
modelStatus.style.color = "#58a6ff";
try {
const buffer = await file.arrayBuffer();
session = await ort.InferenceSession.create(buffer, {
executionProviders: ['webgl', 'wasm', 'cpu']
});
modelStatus.innerHTML = `✅ Modèle IA local prêt (${file.name})`;
modelStatus.style.color = "#2ea043";
updateControls();
} catch (err) {
console.error("Erreur modèle local:", err);
modelStatus.innerHTML = "❌ Fichier .onnx invalide";
modelStatus.style.color = "#f85149";
}
}
// ----------------------------------------------------
// 2. IMPORTS MULTIPLES & SELECTION CUMULATIVE
// ----------------------------------------------------
dropzone.addEventListener('click', () => fileInput.click());
fileInput.addEventListener('change', (e) => {
if (e.target.files.length > 0) {
handleFiles(e.target.files);
fileInput.value = ''; // Réinitialisation immédiate
}
});
['dragenter', 'dragover'].forEach(n => dropzone.addEventListener(n, (e) => { e.preventDefault(); dropzone.classList.add('dragover'); }));
['dragleave', 'drop'].forEach(n => dropzone.addEventListener(n, (e) => { e.preventDefault(); dropzone.classList.remove('dragover'); }));
dropzone.addEventListener('drop', (e) => {
if (e.dataTransfer && e.dataTransfer.files.length > 0) {
handleFiles(e.dataTransfer.files);
}
});
function handleFiles(files) {
for (let file of files) {
if (!file.type.startsWith('image/')) continue;
const id = 'img_' + Math.random().toString(36).substr(2, 9);
const item = {
id: id,
file: file,
status: 'waiting',
originalUrl: URL.createObjectURL(file),
upscaledUrl: null
};
queue.push(item);
renderCard(item);
}
updateControls();
}
function renderCard(item) {
const card = document.createElement('div');
card.className = 'card';
card.id = item.id;
card.innerHTML = `
<div class="preview-box">
<img src="${item.originalUrl}" id="img_prev_${item.id}">
<span class="badge status-waiting" id="badge_${item.id}">En attente</span>
</div>
<div class="card-body">
<div style="font-size:0.85rem; font-weight:600; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; color: var(--heading);" title="${item.file.name}">${item.file.name}</div>
<div id="action_${item.id}"></div>
</div>
`;
document.getElementById('imageGrid').appendChild(card);
}
function updateControls() {
document.getElementById('statsText').innerText = `${queue.length} fichier(s) chargé(s)`;
const hasWaiting = queue.some(i => i.status === 'waiting');
startBtn.disabled = !hasWaiting || isProcessing;
}
// ----------------------------------------------------
// 3. TRAITEMENT BATCH & EXECUTION SÉCURISÉE
// ----------------------------------------------------
async function startBatchProcessing() {
if (isProcessing) return;
isProcessing = true;
startBtn.disabled = true;
for (let item of queue) {
if (item.status !== 'waiting') continue;
item.status = 'processing';
const badge = document.getElementById(`badge_${item.id}`);
badge.innerText = session ? "Inférence IA..." : "Traitement GPU...";
badge.className = "badge status-processing";
let upscaledBlob = null;
let methodUsed = "GPU WebGL 2x";
// Tentative 1 : Inférence IA ONNX si modèle chargé
if (session) {
try {
upscaledBlob = await processImageWithONNX(item.originalUrl);
methodUsed = "IA ONNX 3x";
} catch (err) {
console.warn("L'inférence IA a échoué pour cette image, basculement automatique sur GPU WebGL:", err);
try {
upscaledBlob = await processImageWithWebGL(item.originalUrl);
methodUsed = "GPU WebGL (Fallback)";
} catch (err2) {
console.error("Erreur WebGL Fallback:", err2);
}
}
} else {
// Si aucun modèle IA n'est chargé, traitement GPU WebGL haute qualité immédiat
try {
upscaledBlob = await processImageWithWebGL(item.originalUrl);
methodUsed = "GPU Shader 2x";
} catch (err) {
console.error("Erreur WebGL:", err);
}
}
if (upscaledBlob) {
item.upscaledUrl = URL.createObjectURL(upscaledBlob);
item.status = 'done';
document.getElementById(`img_prev_${item.id}`).src = item.upscaledUrl;
badge.innerText = `Terminé (${methodUsed})`;
badge.className = methodUsed.includes("Fallback") ? "badge status-fallback" : "badge status-done";
const fileName = item.file.name.replace(/(\.[^/.]+)$/, "") + "_upscaled.png";
document.getElementById(`action_${item.id}`).innerHTML =
`<a class="download-btn" href="${item.upscaledUrl}" download="${fileName}">⬇️ Télécharger HD</a>`;
} else {
item.status = 'error';
badge.innerText = "Erreur de rendu";
badge.className = "badge status-error";
}
}
isProcessing = false;
updateControls();
}
// ----------------------------------------------------
// 4. MOTEUR A : INFÉRENCE ONNX IA COMPLÈTE (YCBCR + RGB)
// ----------------------------------------------------
function processImageWithONNX(imgUrl) {
return new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = "anonymous";
// Définition des événements de chargement AVANT l'attribution de la source
img.onload = async () => {
try {
const width = img.naturalWidth;
const height = img.naturalHeight;
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
const imgData = ctx.getImageData(0, 0, width, height);
const data = imgData.data;
// Convertir RGB en Y, Cb, Cr
const numPixels = width * height;
const yData = new Float32Array(numPixels);
for (let i = 0; i < numPixels; i++) {
const r = data[i * 4];
const g = data[i * 4 + 1];
const b = data[i * 4 + 2];
yData[i] = (0.299 * r + 0.587 * g + 0.114 * b) / 255.0;
}
// Détection dynamique de l'entrée et sortie ONNX
const inputName = session.inputNames[0] || 'input';
const inputTensor = new ort.Tensor('float32', yData, [1, 1, height, width]);
const feeds = {};
feeds[inputName] = inputTensor;
// Inférence ONNX
const results = await session.run(feeds);
const outputName = session.outputNames[0] || Object.keys(results)[0];
const outputTensor = results[outputName];
if (!outputTensor || !outputTensor.data) {
throw new Error("Sortie ONNX vide");
}
const outH = outputTensor.dims[2] || height * 3;
const outW = outputTensor.dims[3] || width * 3;
const outYData = outputTensor.data;
// Canvas pour les canaux Chroma (Cb, Cr)
const chromaCanvas = document.createElement('canvas');
chromaCanvas.width = outW;
chromaCanvas.height = outH;
const chromaCtx = chromaCanvas.getContext('2d');
chromaCtx.imageSmoothingQuality = 'high';
chromaCtx.drawImage(img, 0, 0, outW, outH);
const chromaData = chromaCtx.getImageData(0, 0, outW, outH).data;
// Image finale RGBA HD
const outCanvas = document.createElement('canvas');
outCanvas.width = outW;
outCanvas.height = outH;
const outCtx = outCanvas.getContext('2d');
const outImgData = outCtx.createImageData(outW, outH);
const outPixels = outImgData.data;
for (let i = 0; i < outW * outH; i++) {
let yVal = outYData[i];
if (yVal <= 1.0) yVal *= 255.0;
const rOrig = chromaData[i * 4];
const gOrig = chromaData[i * 4 + 1];
const bOrig = chromaData[i * 4 + 2];
const cb = (-0.168736 * rOrig - 0.331264 * gOrig + 0.5 * bOrig) + 128;
const cr = (0.5 * rOrig - 0.418688 * gOrig - 0.081312 * bOrig) + 128;
let r = yVal + 1.402 * (cr - 128);
let g = yVal - 0.344136 * (cb - 128) - 0.714136 * (cr - 128);
let b = yVal + 1.772 * (cb - 128);
outPixels[i * 4] = Math.min(255, Math.max(0, r));
outPixels[i * 4 + 1] = Math.min(255, Math.max(0, g));
outPixels[i * 4 + 2] = Math.min(255, Math.max(0, b));
outPixels[i * 4 + 3] = 255;
}
outCtx.putImageData(outImgData, 0, 0);
outCanvas.toBlob((blob) => {
if (blob) resolve(blob);
else reject(new Error("Erreur Blob"));
}, 'image/png');
} catch (err) {
reject(err);
}
};
img.onerror = () => reject(new Error("Erreur chargement image"));
// Source assignée APRÈS la déclaration des handlers
img.src = imgUrl;
});
}
// ----------------------------------------------------
// 5. MOTEUR B : SHADER WEBGL 2X (GARANTIE ZERO ERREUR)
// ----------------------------------------------------
function processImageWithWebGL(imgUrl) {
return new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = "anonymous";
img.onload = () => {
try {
const canvas = document.getElementById('glCanvas');
const outW = img.naturalWidth * 2;
const outH = img.naturalHeight * 2;
canvas.width = outW;
canvas.height = outH;
const gl = canvas.getContext('webgl', { preserveDrawingBuffer: true }) ||
canvas.getContext('experimental-webgl', { preserveDrawingBuffer: true });
if (!gl) {
const c2d = document.createElement('canvas');
c2d.width = outW;
c2d.height = outH;
const ctx = c2d.getContext('2d');
ctx.imageSmoothingQuality = 'high';
ctx.drawImage(img, 0, 0, outW, outH);
c2d.toBlob(blob => resolve(blob), 'image/png');
return;
}
const vsSource = `
attribute vec2 a_position;
attribute vec2 a_texCoord;
varying vec2 v_texCoord;
void main() {
gl_Position = vec2(a_position.x, a_position.y);
v_texCoord = a_texCoord;
}
`;
const fsSource = `
precision mediump float;
uniform sampler2D u_image;
uniform vec2 u_texSize;
varying vec2 v_texCoord;
float getLuma(vec4 color) {
return dot(color.rgb, vec3(0.299, 0.587, 0.114));
}
void main() {
vec2 d = 1.0 / u_texSize;
vec4 c = texture2D(u_image, v_texCoord);
vec4 cNW = texture2D(u_image, v_texCoord + vec2(-d.x, -d.y));
vec4 cN = texture2D(u_image, v_texCoord + vec2(0.0, -d.y));
vec4 cNE = texture2D(u_image, v_texCoord + vec2(d.x, -d.y));
vec4 cW = texture2D(u_image, v_texCoord + vec2(-d.x, 0.0));
vec4 cE = texture2D(u_image, v_texCoord + vec2(d.x, 0.0));
vec4 cSW = texture2D(u_image, v_texCoord + vec2(-d.x, d.y));
vec4 cS = texture2D(u_image, v_texCoord + vec2(0.0, d.y));
vec4 cSE = texture2D(u_image, v_texCoord + vec2(d.x, d.y));
float lNW = getLuma(cNW), lN = getLuma(cN), lNE = getLuma(cNE);
float lW = getLuma(cW), lC = getLuma(c), lE = getLuma(cE);
float lSW = getLuma(cSW), lS = getLuma(cS), lSE = getLuma(cSE);
float gx = (lNE + 2.0*lE + lSE) - (lNW + 2.0*lW + lSW);
float gy = (lSW + 2.0*lS + lSE) - (lNW + 2.0*lN + lNE);
float grad = sqrt(gx*gx + gy*gy);
if (grad > 0.05) {
vec4 minColor = min(min(cN, cS), min(cW, cE));
c = mix(c, minColor, 0.3);
}
gl_FragColor = c;
}
`;
const vs = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vs, vsSource);
gl.compileShader(vs);
const fs = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fs, fsSource);
gl.compileShader(fs);
const program = gl.createProgram();
gl.attachShader(program, vs);
gl.attachShader(program, fs);
gl.linkProgram(program);
gl.useProgram(program);
const posBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, posBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
-1, -1, 1, -1, -1, 1,
-1, 1, 1, -1, 1, 1
]), gl.STATIC_DRAW);
const posLoc = gl.getAttribLocation(program, "a_position");
gl.enableVertexAttribArray(posLoc);
gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 0, 0);
const texBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, texBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
0, 1, 1, 1, 0, 0,
0, 0, 1, 1, 1, 0
]), gl.STATIC_DRAW);
const texLoc = gl.getAttribLocation(program, "a_texCoord");
gl.enableVertexAttribArray(texLoc);
gl.vertexAttribPointer(texLoc, 2, gl.FLOAT, false, 0, 0);
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
const texSizeLoc = gl.getUniformLocation(program, "u_texSize");
gl.uniform2f(texSizeLoc, outW, outH);
gl.viewport(0, 0, outW, outH);
gl.drawArrays(gl.TRIANGLES, 0, 6);
canvas.toBlob((blob) => {
if (blob) resolve(blob);
else reject(new Error("Erreur toBlob"));
}, 'image/png');
} catch (e) {
reject(e);
}
};
img.onerror = () => reject(new Error("Erreur de chargement de l'image"));
img.src = imgUrl;
});
}
</script>
</body>
</html>