3.9.0: la cola más rápida: la tarjeta descomprime, dos clips a la vez, y las conversiones sueltas se cierran
Medido en la RTX 5080 del estudio con la cola de Vía Sumapaz (1.920 clips, unas 26 horas): el codificador iba al 46-74 % y el descompresor al 0 %. Con NVDEC un H.264 4K pasa de 0,54× a 0,86× tiempo real; dos a la vez salen a 1,29× entre los dos. Además: al arrancar se cierran las conversiones que quedaron sin su programa (una de las 10:04 seguía colgada a las 17:00), «Detener» cierra el ffmpeg del lector de BRAW, la cola se pide entera por carpeta (antes, los 500 últimos de todas), y lo encolado que ya no compensa con la regla del 30 % no se convierte. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Este commit está contenido en:
@@ -697,6 +697,7 @@
|
||||
<label>croma <select id="optChroma"><option value="420">4:2:0 10 bits (compatible)</option><option value="422">4:2:2 10 bits (solo ffmpeg, pesa más)</option></select></label>
|
||||
<label>BRAW en <select id="optColor"><option value="rec709">Rec.709 (como se vio al grabar)</option><option value="log">Log de cámara (plano, más latitud)</option></select></label>
|
||||
<label><input type="checkbox" id="optExport"> incluir exportados</label>
|
||||
<label title="Cuántos clips convierte la cola a la vez. Automático: dos con tarjeta NVIDIA, uno sin ella. Cuenta desde la próxima vez que arranque la cola.">a la vez <select id="optSimult"><option value="0">automático</option><option value="1">1</option><option value="2">2</option><option value="3">3</option></select></label>
|
||||
</div></details></span></div>
|
||||
<p class="ayuda">Convierte los clips de proyectos ya entregados a un formato más ligero. <b>El original no se toca</b> hasta que reemplaces. <span class="q">?</span><span class="mas">Buscar lee los clips (códec, duración, resolución) y calcula cuánto pesarían. La cola convierte un clip tras otro en segundo plano, como Media Encoder; puedes cambiar de vista. Cada clip terminado se verifica solo (códec, bits, duración). Al reemplazar, el original se aparta a <span class="path">_Para borrar/originales</span> y el archivo nuevo queda en su lugar con el mismo nombre más un sufijo. Los BRAW se leen con el decodificador de Blackmagic que instala DaVinci Resolve, sin tener que abrirlo; todo se codifica con ffmpeg, con la tarjeta gráfica cuando la hay. Se ofrece todo lo que convertido pesaría al menos un 30 % menos (se cambia en «cambiar»); lo calcula por el códec de cada clip: un H.264 o un AV1 de cámara suelen quedar en la décima parte, y un H.265 de cámara casi no adelgaza. Si al terminar ahorra menos de lo pedido (con 10 puntos de margen), se descarta y se queda el original. Nada se borra.</span></p>
|
||||
<div class="toolbar" style="margin-bottom:10px"><label>formato de salida <select id="optFormato"></select></label><span id="fmtQue" class="mut" style="font-size:12px"></span><span class="sep"></span><span id="motores" class="mut" style="font-size:12px"><span class="spin"></span> detectando GPU y CPU…</span></div>
|
||||
@@ -994,7 +995,7 @@ async function cargarEstado(){
|
||||
sel.value = (antes && ESTADO.raices.some(r => r.clave === antes)) ? antes : '';
|
||||
pintarIndice();
|
||||
const fmt = $('#optFormato'); if (!fmt.options.length && ESTADO.formatos) { fmt.innerHTML = Object.entries(ESTADO.formatos).map(([k, v]) => `<option value="${k}">${esc(v.nombre)}</option>`).join(''); fmt.value = ESTADO.config.optimizar.formato || 'h265_10bit'; pintarFormato(); cargarMotores(); }
|
||||
const perf = $('#optPerfil'); if (!perf.options.length) { perf.innerHTML = Object.entries(ESTADO.perfiles).map(([k,v]) => `<option value="${k}">${esc(v.nombre)}</option>`).join(''); perf.value = ESTADO.config.optimizar.perfil; $('#optMeses').value = ESTADO.config.optimizar.meses_minimos; $('#optMin').value = ESTADO.config.optimizar.min_mb; $('#optAhorro').value = ESTADO.config.optimizar.ahorro_minimo ?? 30; $('#optChroma').value = ESTADO.config.optimizar.chroma; $('#optColor').value = ESTADO.config.optimizar.color; pintarCriterio(); }
|
||||
const perf = $('#optPerfil'); if (!perf.options.length) { perf.innerHTML = Object.entries(ESTADO.perfiles).map(([k,v]) => `<option value="${k}">${esc(v.nombre)}</option>`).join(''); perf.value = ESTADO.config.optimizar.perfil; $('#optMeses').value = ESTADO.config.optimizar.meses_minimos; $('#optMin').value = ESTADO.config.optimizar.min_mb; $('#optAhorro').value = ESTADO.config.optimizar.ahorro_minimo ?? 30; $('#optSimult').value = String(ESTADO.config.optimizar.simultaneas ?? 0); $('#optChroma').value = ESTADO.config.optimizar.chroma; $('#optColor').value = ESTADO.config.optimizar.color; pintarCriterio(); }
|
||||
const r = cfgRaiz(); const a = r && r.analisis;
|
||||
if (a && a.estado === 'corriendo') $('#anaEstado').innerHTML = `<span class="spin"></span> leyendo clips: ${a.hechos} de ${a.total} <button class="lnk" id="btnAnaDetener">Detener</button>`;
|
||||
else if (a && a.error && vistaActual() === 'convertir') $('#anaEstado').innerHTML = `<span class="warn">${esc(a.error)}</span>`;
|
||||
@@ -1164,7 +1165,7 @@ function tareasDe(E){
|
||||
if (r.progreso && r.progreso.estado === 'corriendo') { const p = r.progreso; const est = r.escaneo && r.escaneo.n ? Math.min(99, Math.round(p.n * 100 / r.escaneo.n)) : null; T.push({raiz: r.clave, q: 'Escaneando', d: `${r.cliente} · ${num(p.n)} archivos · ${gb(p.bytes)}`, pct: est}); }
|
||||
if (r.analisis && r.analisis.estado === 'corriendo') { const a = r.analisis; T.push({raiz: r.clave, q: 'Leyendo clips', d: `${r.cliente} · ${a.hechos} de ${a.total}`, pct: a.total ? Math.round(a.hechos * 100 / a.total) : null}); }
|
||||
});
|
||||
if (E.cola.activa) { const n = E.cola.n || {}; const a = E.cola.actual; const tot = E.cola.total || 0, hechos = E.cola.hechos || 0; const pctClip = a ? (a.pct || 0) : 0; const pctTot = tot ? Math.round((hechos + pctClip / 100) / tot * 100) : null; T.push({raiz: a ? a.raiz : null, q: tot ? `Convirtiendo ${Math.min(hechos + 1, tot)} de ${tot}` : 'Convirtiendo', d: `${a ? (a.ruta || '').split(/[\\/]/).pop() + ' · ' + Math.round(pctClip) + ' % de este clip · ' : ''}${n.pendiente || 0} en espera${E.cola.mensaje ? ' · ' + E.cola.mensaje : ''}`, pct: pctTot, cancelar: () => api('/api/optimizar/control', {accion: 'detener'})}); }
|
||||
if (E.cola.activa) { const n = E.cola.n || {}; const a = E.cola.actual; const vs = (E.cola.actuales && E.cola.actuales.length) ? E.cola.actuales : (a ? [a] : []); const tot = E.cola.total || 0, hechos = E.cola.hechos || 0; const pctVan = vs.reduce((s, x) => s + (x.pct || 0), 0); const pctTot = tot ? Math.round((hechos + pctVan / 100) / tot * 100) : null; const numeros = vs.length > 1 ? `${Math.min(hechos + 1, tot)} y ${Math.min(hechos + 2, tot)}` : `${Math.min(hechos + 1, tot)}`; T.push({raiz: a ? a.raiz : null, q: tot ? `Convirtiendo ${numeros} de ${tot}` : 'Convirtiendo', d: `${vs.map(x => (x.ruta || '').split(/[\\/]/).pop() + ' · ' + Math.round(x.pct || 0) + ' %').join(' · ')}${vs.length ? ' · ' : ''}${n.pendiente || 0} en espera${E.cola.mensaje ? ' · ' + E.cola.mensaje : ''}`, pct: pctTot, cancelar: () => api('/api/optimizar/control', {accion: 'detener'})}); }
|
||||
if (E.asistente.activo) { const a = E.asistente; T.push({raiz: a.raiz, q: 'Asistente: ' + (NOMBRE_PASO[a.paso] || a.paso || '…'), d: `${porNombre(a.raiz)} · ${a.total ? num(a.hechas) + ' de ' + num(a.total) + ' · ' : ''}${(a.texto || '').slice(-60)}`, pct: a.total ? a.pct : null}); }
|
||||
else if (E.aplicando && E.aplicando.total && E.aplicando.hechas < E.aplicando.total) T.push({q: 'Moviendo', d: E.aplicando.actual || '', pct: Math.round(E.aplicando.unidades_total ? E.aplicando.unidades * 100 / E.aplicando.unidades_total : E.aplicando.hechas * 100 / E.aplicando.total)});
|
||||
if (E.respaldo.estado === 'corriendo') { const e = E.respaldo; T.push({q: 'Copiando respaldo', d: `${num(e.hechos)} de ${num(e.total)} · ${gb(e.bytes)} de ${gb(e.bytes_total)}`, pct: e.bytes_total ? Math.round(e.bytes / e.bytes_total * 100) : null, cancelar: () => api('/api/respaldo/detener', {})}); }
|
||||
@@ -1894,6 +1895,7 @@ $('#btnStockPapelera').onclick = () => { const s = STOCK.filter(g => g.sel && g.
|
||||
const ahorroPedido = () => { const v = $('#optAhorro').value; return v === '' ? 30 : Math.max(0, Math.min(90, Math.round(+v) || 0)); };
|
||||
function pintarCriterio(){ $('#convCriterio').textContent = `con más de ${+$('#optMeses').value || 0} meses, más de ${+$('#optMin').value || 0} MB y que ahorren al menos un ${ahorroPedido()} %`; }
|
||||
['optMeses','optMin','optAhorro'].forEach(id => $('#' + id).oninput = pintarCriterio);
|
||||
$('#optSimult').onchange = () => api('/api/config', {optimizar: {simultaneas: +$('#optSimult').value}}).then(() => aviso(`La cola convertirá ${+$('#optSimult').value || 'automáticamente uno o dos'}${+$('#optSimult').value === 1 ? ' clip' : (+$('#optSimult').value ? ' clips' : '')} a la vez desde la próxima vez que arranque.`, 'ok')).catch(e => aviso(e.message, 'bad'));
|
||||
// el ahorro mínimo vale también para la pasada automática y para Inicio: se guarda al cambiarlo
|
||||
$('#optAhorro').onchange = () => api('/api/config', {optimizar: {ahorro_minimo: ahorroPedido()}}).then(() => { if (CAND.length) return cargarCandidatos(); }).catch(e => aviso(e.message, 'bad'));
|
||||
let COLA_TS = [], CONV_VER = 'todos', CONV_ORDEN = {campo: 'carpeta', inverso: false}, CONV_EXC = '', CONV_PLEGADAS = new Set(), CONV_BUSCA = '', CONV_CRITERIO = '';
|
||||
@@ -1959,8 +1961,8 @@ function resumenCand(){ const s = CAND.filter(c => c.sel); const validos = CAND.
|
||||
$('#btnEncolar').onclick = () => { const s = CAND.filter(c => c.sel); const fm = (ESTADO.formatos || {})[$('#optFormato').value] || {nombre: $('#optFormato').value}; modal('Convertir', `<p><b>${s.length}</b> clips (${gb(s.reduce((a,c)=>a+c.bytes,0))}) se convierten a <b>${esc(fm.nombre)}</b>, uno tras otro, en segundo plano. Pesarían ≈ ${gb(s.reduce((a,c)=>a+c.estimado,0))}.</p><p><b>El original no se toca.</b> Cuando terminen y estén verificados, decides si reemplazar.</p>`, () => accion($('#btnEncolar'), async () => { await api('/api/config', {optimizar: {meses_minimos: +$('#optMeses').value || 0, min_mb: +$('#optMin').value || 0, ahorro_minimo: ahorroPedido(), perfil: $('#optPerfil').value, formato: $('#optFormato').value, chroma: $('#optChroma').value, color: $('#optColor').value}}); const r = await api('/api/optimizar/encolar', {raiz: raizActual(), rutas: s.map(c => c.ruta), perfil: $('#optPerfil').value, formato: $('#optFormato').value, chroma: $('#optChroma').value, color: $('#optColor').value, iniciar: true}); aviso(`${r.ids.length} clips en cola. El avance está en la barra de abajo.`, 'ok'); if (r.ids.length < s.length) aviso(`${s.length - r.ids.length} no se encolaron: ya estaban en cola o su salida ya existe (el mismo clip en MP4 y MOV, o una conversión anterior).`, ''); await cargarCandidatos(); await cargarEstado(); })); $('#modalSi').textContent = 'Convertir'; };
|
||||
const EST_TXT = {pendiente:'en espera', corriendo:'convirtiendo', listo:'convertido', verificado:'verificado', reemplazado:'reemplazado', error:'falló', cancelado:'cancelado', descartado:'no compensa'};
|
||||
async function cargarCola(){
|
||||
const d = await api('/api/optimizar/cola');
|
||||
const raiz = raizActual();
|
||||
const d = await api('/api/optimizar/cola' + (raiz ? '?raiz=' + encodeURIComponent(raiz) : ''));
|
||||
const ts = d.trabajos.filter(t => !raiz || (t.raiz || '').toLowerCase() === raiz.toLowerCase());
|
||||
const orden = {corriendo:0, pendiente:1, listo:2, verificado:2, error:3, descartado:4, reemplazado:5, cancelado:6};
|
||||
ts.sort((x, y) => (orden[x.estado] ?? 9) - (orden[y.estado] ?? 9) || y.id - x.id);
|
||||
@@ -2011,7 +2013,7 @@ async function cargarCola(){
|
||||
const corre = t && t.estado === 'corriendo', pct = corre ? Math.max(0, Math.min(100, Math.round(t.pct || 0))) : 0;
|
||||
const est = corre ? `<span class="est prog ${pct >= 50 ? 'media' : ''}" style="--p:${pct}%" title="convirtiendo: ${pct} %">${pct} %</span>` : t ? `<span class="est ${t.estado}">${EST_TXT[t.estado] || esc(t.estado)}</span>` : `<span class="est candidato">${c.excluido ? 'no' : 'candidato'}</span>`;
|
||||
let sub = c ? `${esc(c.fecha)} · ${esc(c.codec)} ${esc(c.res)} · ${dur(c.dur)} · ${motorT}` : motorT;
|
||||
if (corre) { const a = eta && eta.actual && eta.actual.id === t.id ? eta.actual : null; sub = `<span class="acc">convirtiendo${a && a.dur ? ` · ${tiempoLargo(a.dur - a.resto)} de ${tiempoLargo(a.dur)} · quedan ${tiempoLargo(a.resto_seg)}` : ''}${E && E.mensaje ? ' · ' + esc(E.mensaje) : ''}</span> · ${motorT}`; }
|
||||
if (corre) { const a = eta && ((eta.actuales || []).find(x => x.id === t.id) || (eta.actual && eta.actual.id === t.id ? eta.actual : null)); sub = `<span class="acc">convirtiendo${a && a.dur ? ` · ${tiempoLargo(a.dur - a.resto)} de ${tiempoLargo(a.dur)} · quedan ${tiempoLargo(a.resto_seg)}` : ''}${E && E.mensaje ? ' · ' + esc(E.mensaje) : ''}</span> · ${motorT}`; }
|
||||
else if (t && t.estado === 'error') sub = `<span class="bad" title="${esc(t.log || '')}">${esc(((t.log || '').trim().split('\n').pop() || 'falló sin decir por qué').slice(0, 320))}</span>`;
|
||||
else if (t && t.estado === 'descartado') sub = `<span class="mut">${esc(t.log || '')}</span>`;
|
||||
else if (t && t.estado === 'pendiente' && t.log) sub = `<span class="mut">${esc(t.log)}</span>`;
|
||||
@@ -2034,9 +2036,12 @@ async function cargarCola(){
|
||||
if (d.cola.activa && d.cola.esperando) {
|
||||
mo = `<div class="momento"><div class="l1"><span class="spin"></span><b>La cola está en pausa</b><span class="mut">${esc(d.cola.esperando)} · ${pend} en espera · se cambia en «Cuándo trabaja la cola»</span><span class="sep"></span><button class="bad" id="moDetener">Detener</button></div></div>`;
|
||||
} else if (d.cola.activa) {
|
||||
const total = E && E.total ? E.total : (pend + (corr ? 1 : 0)), hechosT = E ? (E.hechos || 0) : 0;
|
||||
const pctTot = total ? Math.round((hechosT + (corr ? (corr.pct || 0) / 100 : 0)) / total * 100) : 0;
|
||||
mo = `<div class="momento corre"><div class="l1"><span class="spin"></span><b>Convirtiendo ${Math.min(hechosT + 1, total)} de ${total}</b><span class="mut">${eta && eta.seg ? `quedan unas <b>${tiempoLargo(eta.seg)}</b> · termina hacia las <b>${hora(eta.termina)}</b>${eta.medido ? ` · va a ${eta.vel.toFixed(1)}× tiempo real` : ' · estimado, hasta que acabe el primero'}` : 'calculando cuánto falta…'}</span><span class="sep"></span><button class="bad" id="moDetener">Detener</button></div><div class="bar grande"><i style="width:${pctTot}%"></i></div><div class="l3 mut">${corr ? `ahora: <b>${esc((corr.ruta || '').split(/[\\/]/).pop())}</b> · ${Math.round(corr.pct || 0)} %${eta && eta.actual ? ` · quedan ${tiempoLargo(eta.actual.resto_seg)} de este clip` : ''} · ${eta ? eta.en_espera : pend} en espera` : 'empezando…'}</div></div>`;
|
||||
const corrs = ts.filter(t => t.estado === 'corriendo');
|
||||
const total = E && E.total ? E.total : (pend + (corrs.length || (corr ? 1 : 0))), hechosT = E ? (E.hechos || 0) : 0;
|
||||
const pctTot = total ? Math.round((hechosT + corrs.reduce((s, x) => s + (x.pct || 0), 0) / 100) / total * 100) : 0;
|
||||
const restoDe = x => { const a = eta && (eta.actuales || []).find(y => y.id === x.id); return a ? ` · quedan ${tiempoLargo(a.resto_seg)}` : ''; };
|
||||
const ahora = corrs.length > 1 ? `a la vez: ${corrs.map(x => `<b>${esc((x.ruta || '').split(/[\\/]/).pop())}</b> ${Math.round(x.pct || 0)} %${restoDe(x)}`).join(' · ')} · ${eta ? eta.en_espera : pend} en espera` : '';
|
||||
mo = `<div class="momento corre"><div class="l1"><span class="spin"></span><b>Convirtiendo ${corrs.length > 1 ? `${Math.min(hechosT + 1, total)} y ${Math.min(hechosT + 2, total)}` : Math.min(hechosT + 1, total)} de ${total}</b><span class="mut">${eta && eta.seg ? `quedan unas <b>${tiempoLargo(eta.seg)}</b> · termina hacia las <b>${hora(eta.termina)}</b>${eta.medido ? ` · va a ${eta.vel.toFixed(1)}× tiempo real` : ' · estimado, hasta que acabe el primero'}` : 'calculando cuánto falta…'}</span><span class="sep"></span><button class="bad" id="moDetener">Detener</button></div><div class="bar grande"><i style="width:${pctTot}%"></i></div><div class="l3 mut">${ahora || (corr ? `ahora: <b>${esc((corr.ruta || '').split(/[\\/]/).pop())}</b> · ${Math.round(corr.pct || 0)} %${restoDe(corr)} de este clip · ${eta ? eta.en_espera : pend} en espera` : 'empezando…')}</div></div>`;
|
||||
} else if (pend) {
|
||||
mo = `<div class="momento"><div class="l1"><b>${pend} clip${pend > 1 ? 's' : ''} en espera, con la cola parada</b><span class="mut">lo que ya iba se retoma desde cero</span><span class="sep"></span><button class="p grande" id="moReanudar">Reanudar la cola</button></div></div>`;
|
||||
} else if (vers.length) {
|
||||
@@ -2196,7 +2201,7 @@ function comparar(id){
|
||||
const br = $('#cmpReemplazar'); if (br) br.onclick = () => accion(br, async () => { const r = await api('/api/optimizar/reemplazar', {ids: [id]}); const x = r.resultados[0] || {}; aviso(x.ok ? `Reemplazado: ${x.motivo}` : `No se reemplazó: ${x.motivo}`, x.ok ? 'ok' : 'bad', x.ok ? {texto: 'Ver historial', ir: () => irA('historial')} : null); $('#modal').classList.remove('on'); cargarCola(); });
|
||||
}
|
||||
$('#btnColaToggle').onclick = () => accion($('#btnColaToggle'), async () => { await api('/api/optimizar/control', {accion: $('#btnColaToggle').textContent === 'Detener' ? 'detener' : 'iniciar'}); await cargarEstado(); cargarCola(); });
|
||||
const idsPorEstado = async (...est) => { const d = await api('/api/optimizar/cola'); const raiz = raizActual().toLowerCase(); return d.trabajos.filter(t => est.includes(t.estado) && (t.raiz || '').toLowerCase() === raiz && !(est.includes('verificado') && (t.formato || '').startsWith('prores'))).map(t => t.id); };
|
||||
const idsPorEstado = async (...est) => { const d = await api('/api/optimizar/cola?raiz=' + encodeURIComponent(raizActual())); const raiz = raizActual().toLowerCase(); return d.trabajos.filter(t => est.includes(t.estado) && (t.raiz || '').toLowerCase() === raiz && !(est.includes('verificado') && (t.formato || '').startsWith('prores'))).map(t => t.id); };
|
||||
$('#btnVerificar').onclick = () => accion($('#btnVerificar'), async () => { const ids = await idsPorEstado('listo'); const r = await api('/api/optimizar/verificar', {ids}); const ok = r.resultados.filter(x => x.ok).length; aviso(`${ok} de ${r.resultados.length} verificados.`, ok === r.resultados.length ? 'ok' : 'bad'); if (ok < r.resultados.length) modal('Verificación', `<pre>${esc(r.resultados.map(x => `#${x.id} ${x.ok ? 'OK' : 'FALLA'} · ${x.motivo}`).join('\n'))}</pre>`, null); cargarCola(); });
|
||||
$('#btnReemplazar').onclick = () => accion($('#btnReemplazar'), async () => { const ids = await idsPorEstado('verificado'); modal('Reemplazar originales', `<p><b>${ids.length}</b> originales se apartan a <span class="path">_Para borrar/originales</span> dentro de su carpeta (no se borran). El MP4 10 bits queda en su lugar.</p><p>Cuando estés seguro, una persona vacía _Para borrar a mano.</p>`, () => accion($('#btnReemplazar'), async () => { const r = await api('/api/optimizar/reemplazar', {ids}); const ok = r.resultados.filter(x => x.ok).length; aviso(`${ok} de ${r.resultados.length} reemplazados.`, ok === r.resultados.length ? 'ok' : 'bad', {texto: 'Ver historial', ir: () => irA('historial')}); if (ok < r.resultados.length) modal('Reemplazo', `<pre>${esc(r.resultados.map(x => `#${x.id} ${x.ok ? 'OK' : 'NO'} · ${x.motivo}`).join('\n'))}</pre>`, null); cargarCola(); })); $('#modalSi').textContent = 'Reemplazar'; });
|
||||
$('#btnReintentar').onclick = () => accion($('#btnReintentar'), async () => { const ids = await idsPorEstado('error', 'cancelado'); await api('/api/optimizar/control', {accion: 'reintentar', ids}); await api('/api/optimizar/control', {accion: 'iniciar'}); cargarCola(); });
|
||||
|
||||
Referencia en una nueva incidencia
Block a user