Labmagic 1.0: control de la Blackmagic Pocket 4K por Bluetooth, panel para OBS y licencia MIT

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Este commit está contenido en:
2026-10-10 10:54:09 -05:00
commit 2ce1d650b5
Se han modificado 24 ficheros con 4356 adiciones y 0 borrados

370
dock.html Archivo normal
Ver fichero

@@ -0,0 +1,370 @@
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Labmagic</title>
<link rel="icon" href="/icon.png">
<style>
:root {
--bg: #0d0d0d; --hud: #000; --card: #181818; --border: #2a2a2a; --btn: #2b2b2b; --btn-hover: #3c3c3c;
--trough: #303030; --knob: #ececec; --fg: #f3f3f3; --muted: #9b9b9b; --dim: #555; --rec: #e3262c;
--on: #3ad66c; --busy: #f0a500; --off: #4a4a4a; --sheet: #161616;
}
* { box-sizing: border-box; -webkit-user-select: none; user-select: none; }
html, body { margin: 0; height: 100%; background: var(--bg); color: var(--fg);
font: 13px "Segoe UI", system-ui, sans-serif; overflow-x: hidden; }
button { font: inherit; color: var(--fg); background: var(--btn); border: 1px solid transparent; border-radius: 6px;
padding: 7px 10px; cursor: pointer; }
button:hover { background: var(--btn-hover); }
button:active { background: #555; }
button:disabled { opacity: .45; cursor: default; }
button.on { border-color: var(--rec); background: #2a1416; }
button.big { font-size: 16px; font-weight: 600; padding: 12px 10px; letter-spacing: .5px; }
button.sq { padding: 7px 0; width: 30px; }
/* HUD */
.hud { background: var(--hud); padding: 7px 10px 6px; }
.top { display: flex; align-items: center; gap: 8px; }
.led { width: 11px; height: 11px; border-radius: 50%; background: var(--off); flex: none; }
.led.on { background: var(--on); box-shadow: 0 0 6px var(--on); }
.led.busy { background: var(--busy); }
.name { font-weight: 600; font-size: 14px; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.name:hover { text-decoration: underline; }
.tc { margin-left: auto; font: 600 22px "Consolas", "Segoe UI", monospace; letter-spacing: 1px; white-space: nowrap; }
.tc.rec { color: var(--rec); }
.dot { color: transparent; font-size: 12px; width: 10px; }
.dot.rec { color: var(--rec); animation: blink 1s steps(1) infinite; }
@keyframes blink { 50% { color: transparent; } }
.status { color: var(--muted); font-size: 11px; margin-top: 1px; display: flex; gap: 8px; }
.status .pw { margin-left: auto; }
.cells { display: grid; grid-template-columns: repeat(6, 1fr); gap: 3px; margin-top: 6px; }
@media (max-width: 330px) { .cells { grid-template-columns: repeat(3, 1fr); } }
.cell { background: #0a0a0a; border: 1px solid #1c1c1c; border-radius: 6px; padding: 4px 2px 5px; text-align: center; }
.cell .l { color: var(--muted); font-size: 9px; letter-spacing: .8px; }
.cell .v { font: 600 15px "Segoe UI", sans-serif; margin-top: 1px; white-space: nowrap; overflow: hidden; }
.cell.click { cursor: pointer; }
.cell.click .v { border-bottom: 1px solid #3a3a3a; display: inline-block; padding: 0 2px; }
.cell.click:hover { background: #151515; }
.cell.click:hover .v { border-bottom-color: var(--fg); }
.banner { display: none; padding: 6px 10px; font-size: 12px; background: var(--rec); }
.banner.warn { background: #8a5a00; }
.banner.ok { background: #1f4d2b; }
.banner.show { display: block; }
main { padding: 8px 10px 10px; display: flex; flex-direction: column; gap: 8px; }
.row { display: flex; gap: 6px; align-items: stretch; }
.row > .grow { flex: 1; }
.tog { font-size: 11.5px; padding: 6px 8px; flex: 1; white-space: nowrap; }
.swatch { display: inline-block; width: 10px; height: 10px; border-radius: 50%; vertical-align: -1px; margin-right: 4px;
border: 1px solid #666; }
.srow { display: grid; grid-template-columns: 44px 30px 1fr 30px; gap: 5px; align-items: center; }
.srow .lab { font-size: 12px; color: var(--muted); }
.srow .lab b { display: block; color: var(--fg); font-weight: 600; font-size: 12px; white-space: nowrap; }
input[type=range] { -webkit-appearance: none; width: 100%; height: 28px; background: transparent; margin: 0; cursor: pointer; }
input[type=range]:disabled { opacity: .4; cursor: default; }
input[type=range]::-webkit-slider-runnable-track { height: 6px; background: var(--trough); border-radius: 3px; }
input[type=range]::-webkit-slider-thumb { -webkit-appearance: none; width: 18px; height: 18px; border-radius: 50%;
background: var(--knob); margin-top: -6px; box-shadow: 0 1px 3px #000; }
.rec-row { display: flex; align-items: center; gap: 12px; background: var(--card); border-radius: 8px; padding: 8px 10px; }
.recbtn { width: 46px; height: 46px; border-radius: 50%; background: #1a1a1a; border: 3px solid #444; position: relative;
flex: none; padding: 0; }
.recbtn::after { content: ""; position: absolute; inset: 7px; border-radius: 50%; background: var(--rec); transition: inset .15s, border-radius .15s; }
.recbtn:hover { border-color: #777; }
.recbtn.rec::after { inset: 12px; border-radius: 4px; }
.recinfo .s { font-weight: 700; letter-spacing: 1px; }
.recinfo .s.rec { color: var(--rec); }
.recinfo .i { color: var(--muted); font-size: 11px; margin-top: 2px; }
.foot { display: flex; gap: 8px; color: var(--dim); font-size: 10.5px; align-items: center; }
.foot a { color: var(--muted); cursor: pointer; margin-left: auto; white-space: nowrap; }
.foot a:hover { color: var(--fg); }
/* Selector estilo cámara (hoja inferior) */
.veil { position: fixed; inset: 0; background: rgba(0,0,0,.55); display: none; }
.veil.show { display: block; }
.sheet { position: fixed; left: 0; right: 0; bottom: 0; background: var(--sheet); border-top: 1px solid var(--border);
border-radius: 12px 12px 0 0; padding: 10px 12px 12px; transform: translateY(100%); transition: transform .18s ease-out;
max-height: 85%; overflow-y: auto; }
.sheet.show { transform: none; }
.sh-head { display: flex; align-items: baseline; gap: 10px; margin-bottom: 8px; }
.sh-head .t { color: var(--muted); font-size: 11px; letter-spacing: 1px; }
.sh-head .cur { font: 600 20px "Segoe UI", sans-serif; }
.sh-head .x { margin-left: auto; color: var(--muted); cursor: pointer; padding: 0 4px; font-size: 16px; }
.sh-head .x:hover { color: var(--fg); }
.chips { display: grid; grid-template-columns: repeat(auto-fill, minmax(54px, 1fr)); gap: 4px; margin-bottom: 6px; }
.chip { background: #222; border: 1px solid transparent; border-radius: 6px; padding: 7px 2px; text-align: center;
font-weight: 600; cursor: pointer; font-size: 13px; white-space: nowrap; }
.chip:hover { background: #333; }
.chip.dim { color: var(--muted); font-weight: 400; background: #1c1c1c; }
.chip.sel { background: var(--fg); color: #000; }
.chip.wide { grid-column: span 2; }
.sub { color: var(--muted); font-size: 10.5px; letter-spacing: .8px; margin: 6px 0 3px; }
.note { color: var(--dim); font-size: 10.5px; margin-top: 4px; }
.kv { display: grid; grid-template-columns: 36px 1fr 36px; gap: 6px; align-items: center; font-size: 10.5px; color: var(--muted); }
.ticks { display: flex; justify-content: space-between; padding: 0 42px; color: var(--dim); font-size: 9px; }
</style>
</head>
<body>
<header class="hud">
<div class="top">
<span class="led" id="led"></span>
<span class="name" id="name" title="Pulsa para conectar o desconectar">Sin cámara</span>
<span class="tc" id="tc">00:00:00:00</span><span class="dot" id="dot">●</span>
</div>
<div class="status"><span id="status">Buscando Labmagic…</span><span class="pw" id="power"></span></div>
<div class="cells">
<div class="cell"><div class="l">FPS</div><div class="v" id="c_fps">—</div></div>
<div class="cell click" data-pick="shutter"><div class="l">SHUTTER</div><div class="v" id="c_sh">—</div></div>
<div class="cell click" data-pick="iris"><div class="l">IRIS</div><div class="v" id="c_iris">—</div></div>
<div class="cell click" data-pick="iso"><div class="l">ISO</div><div class="v" id="c_iso">—</div></div>
<div class="cell click" data-pick="wb"><div class="l">WB</div><div class="v" id="c_wb">—</div></div>
<div class="cell click" data-pick="wb"><div class="l">TINT</div><div class="v" id="c_tint">—</div></div>
</div>
</header>
<div class="banner" id="banner"></div>
<main>
<div class="row">
<button class="big grow" id="af" title="Autofoco (también con la barra espaciadora)">AUTOFOCO&nbsp; (AF)</button>
<button class="tog" id="peak" style="flex:0 0 auto" title="Focus assist con líneas de color en la salida elegida en Labmagic"><span class="swatch" id="peaksw"></span>PEAK</button>
</div>
<div class="row">
<button class="tog" id="zebra" title="Zebra en la salida elegida">ZEBRA</button>
<button class="tog" id="false" title="Falso color en la salida elegida">FALSO COLOR</button>
<button class="tog" id="afauto" title="Vuelve a enfocar cada pocos segundos (intervalo en Labmagic → Ajustes)">AF AUTO</button>
</div>
<div class="srow">
<div class="lab">Foco<b id="focus_txt">—</b></div>
<button class="sq" id="f_minus" title="Más cerca (Mayús: paso grande)">◀</button>
<input type="range" id="focus" min="0" max="1000" value="500" disabled>
<button class="sq" id="f_plus" title="Más lejos (Mayús: paso grande)">▶</button>
</div>
<div class="srow">
<div class="lab">Iris<b id="iris_txt">—</b></div>
<button class="sq" id="i_minus" title="Abrir">◀</button>
<input type="range" id="iris" min="0" max="1000" value="0" disabled>
<button class="sq" id="i_plus" title="Cerrar">▶</button>
</div>
<div class="rec-row">
<button class="recbtn" id="rec" title="Grabar / parar"></button>
<div class="recinfo"><div class="s" id="recstate">GRABAR</div><div class="i" id="recinfo">—</div></div>
</div>
<div class="foot"><span id="lens"></span><a id="open" title="Trae la ventana de Labmagic al frente">abrir Labmagic ⇱</a></div>
</main>
<div class="veil" id="veil"></div>
<div class="sheet" id="sheet"></div>
<script>
(() => {
const $ = id => document.getElementById(id);
const ISO_FULL = [100, 200, 400, 800, 1600, 3200, 6400, 12800, 25600];
const ISO_ALL = [100,125,160,200,250,320,400,500,640,800,1000,1250,1600,2000,2500,3200,4000,5000,6400,8000,10000,12800,16000,20000,25600];
const ANG_MAIN = [45, 90, 172.8, 180, 270, 360];
const ANG_ALL = [11.2,15,22.5,30,37.5,45,60,72,90,108,120,144,150,172.8,180,216,270,360];
const SPEEDS = [24,25,30,48,50,60,100,120,125,200,250,500,1000,2000];
const FSTOPS = [1.4,1.8,2.0,2.8,3.5,4.0,4.5,5.6,6.3,8.0,11,16,22];
const WB_CHIPS = [["☀ Día",5600,10],["💡 Tungst.",3200,0],["▭ Fluor.",4000,15],["☁ Nublado",6500,10],["⌂ Sombra",7500,10]];
const PEAK_COL = ["#e3262c","#3ad66c","#3b82f6","#f3f3f3","#000"];
const PEAK_NAME = ["Rojo","Verde","Azul","Blanco","Negro"];
let S = {}; // último estado recibido
let dragging = null; // deslizador que se está arrastrando ("focus" | "iris" | "wb" | "tint")
let bannerT = null;
let alive = false;
const cmd = (c, extra = {}) =>
fetch("/cmd", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ cmd: c, ...extra }) })
.catch(() => {});
// Envío con límite de ritmo para los deslizadores
const pending = {};
let flushT = null;
function throttled(key, fn) {
pending[key] = fn;
if (!flushT) flushT = setTimeout(() => { flushT = null; const p = { ...pending }; for (const k in pending) delete pending[k]; Object.values(p).forEach(f => f()); }, 120);
}
function banner(text, level = "error", secs = 6) {
const b = $("banner");
b.textContent = text; b.className = "banner show " + level;
clearTimeout(bannerT);
bannerT = setTimeout(() => b.className = "banner", secs * 1000);
}
function render(s) {
S = s;
const on = !!s.connected;
$("led").className = "led " + (on ? "on" : (s.busy ? "busy" : ""));
$("name").textContent = s.name || "Sin cámara";
$("status").textContent = s.status || "";
$("power").textContent = s.power || "";
$("tc").textContent = s.tc || "00:00:00:00";
$("tc").className = "tc" + (s.rec ? " rec" : "");
$("dot").className = "dot" + (s.rec ? " rec" : "");
$("c_fps").textContent = s.fps || "—";
$("c_sh").textContent = s.shutter || "—";
$("c_iris").textContent = s.iris || "—";
$("c_iso").textContent = s.iso || "—";
$("c_wb").textContent = s.wb ? s.wb + "K" : "—";
$("c_tint").textContent = (s.tint === 0 || s.tint) ? (s.tint > 0 ? "+" + s.tint : String(s.tint)) : "—";
$("peak").classList.toggle("on", !!s.peak);
$("peaksw").style.background = PEAK_COL[s.peak_colour || 0];
$("zebra").classList.toggle("on", !!s.zebra);
$("false").classList.toggle("on", !!s.false);
$("afauto").classList.toggle("on", !!s.af_auto);
for (const id of ["af","peak","zebra","false","afauto","f_minus","f_plus","i_minus","i_plus","rec"]) $(id).disabled = !on;
$("focus").disabled = !on; $("iris").disabled = !on;
if (dragging !== "focus") { if (s.focus !== null && s.focus !== undefined) $("focus").value = Math.round(s.focus * 1000); }
if (dragging !== "iris") { if (s.aperture !== null && s.aperture !== undefined) $("iris").value = Math.round(s.aperture * 1000); }
$("focus_txt").textContent = s.focus_text || "—";
$("iris_txt").textContent = s.iris || "—";
$("rec").classList.toggle("rec", !!s.rec);
$("recstate").textContent = s.rec ? "GRABANDO" : "GRABAR";
$("recstate").className = "s" + (s.rec ? " rec" : "");
$("recinfo").textContent = s.rec_info || (on ? "" : "—");
$("lens").textContent = s.lens || "";
if (s.notice && s.notice.t !== (render.lastNotice || 0)) { render.lastNotice = s.notice.t; banner(s.notice.text, s.notice.level, s.notice.secs || 6); }
if (sheetKind) refreshSheet();
}
// --- conexión con Labmagic (SSE con reintento)
function listen() {
const es = new EventSource("/events");
es.onopen = () => { alive = true; };
es.onmessage = e => { try { render(JSON.parse(e.data)); } catch (_) {} };
es.onerror = () => {
es.close(); alive = false;
$("led").className = "led"; $("status").textContent = "Labmagic no está abierta: ábrela y este panel se conecta solo";
setTimeout(listen, 2000);
};
}
listen();
// --- botones
$("name").onclick = () => cmd("connect");
$("af").onclick = () => { cmd("af"); const b = $("af"); b.textContent = "AF ● enviado"; setTimeout(() => b.innerHTML = "AUTOFOCO&nbsp; (AF)", 900); };
$("peak").onclick = () => cmd("peak", { on: !S.peak });
$("peak").oncontextmenu = e => { e.preventDefault(); openSheet("peak"); };
$("zebra").onclick = () => cmd("zebra", { on: !S.zebra });
$("false").onclick = () => cmd("false", { on: !S.false });
$("afauto").onclick = () => cmd("af_auto", { on: !S.af_auto });
$("rec").onclick = () => cmd("record");
$("open").onclick = () => cmd("show");
const step = e => (e.shiftKey ? 0.05 : 0.01);
$("f_minus").onclick = e => cmd("focus_delta", { d: -step(e) });
$("f_plus").onclick = e => cmd("focus_delta", { d: step(e) });
$("i_minus").onclick = e => cmd("iris_delta", { d: -0.05 });
$("i_plus").onclick = e => cmd("iris_delta", { d: 0.05 });
for (const [id, key, c] of [["focus", "focus", "focus"], ["iris", "iris", "iris"]]) {
const el = $(id);
el.oninput = () => { dragging = key; const v = el.value / 1000; if (key === "focus") $("focus_txt").textContent = v.toFixed(2); throttled(c, () => cmd(c, { v })); };
el.onchange = () => { const v = el.value / 1000; cmd(c, { v }); setTimeout(() => dragging = null, 300); };
}
// Teclado: espacio = AF, flechas = foco (cuando el panel tiene el foco)
document.addEventListener("keydown", e => {
if (!S.connected) return;
if (e.code === "Space") { e.preventDefault(); $("af").click(); }
else if (e.code === "ArrowLeft") { e.preventDefault(); cmd("focus_delta", { d: -step(e) }); }
else if (e.code === "ArrowRight") { e.preventDefault(); cmd("focus_delta", { d: step(e) }); }
});
// --- selectores (hoja inferior)
let sheetKind = null;
const wbState = { k: 5600, t: 0 };
document.querySelectorAll(".cell.click").forEach(c => c.onclick = () => openSheet(c.dataset.pick));
$("veil").onclick = closeSheet;
function closeSheet() { sheetKind = null; $("sheet").classList.remove("show"); $("veil").classList.remove("show"); }
function openSheet(kind) {
if (!S.connected) { banner("Conecta la cámara para cambiar valores", "warn", 3); return; }
sheetKind = kind;
if (kind === "wb") { wbState.k = S.wb || 5600; wbState.t = S.tint || 0; }
buildSheet(kind);
$("sheet").classList.add("show"); $("veil").classList.add("show");
}
const h = (tag, cls, text) => { const e = document.createElement(tag); if (cls) e.className = cls; if (text !== undefined) e.textContent = text; return e; };
function chips(parent, items, cur, onpick, dim) {
const g = h("div", "chips");
for (const [label, val] of items) {
const c = h("div", "chip" + (dim && dim.has(val) ? " dim" : "") + (cur !== null && cur !== undefined && Math.abs(cur - val) < 1e-6 ? " sel" : ""), label);
c.onclick = () => onpick(val);
g.appendChild(c);
}
parent.appendChild(g);
return g;
}
function head(parent, title, cur) {
const hd = h("div", "sh-head");
hd.appendChild(h("span", "t", title)); const cv = h("span", "cur", cur); hd.appendChild(cv);
const x = h("span", "x", "✕"); x.onclick = closeSheet; hd.appendChild(x);
parent.appendChild(hd);
return cv;
}
function refreshSheet() { if (sheetKind && sheetKind !== "wb") buildSheet(sheetKind); }
function buildSheet(kind) {
const sh = $("sheet"); sh.innerHTML = "";
if (kind === "iso") {
const cur = parseInt(S.iso) || null;
head(sh, "ISO", S.iso || "—");
const pick = v => { cmd("iso", { v }); closeSheet(); };
chips(sh, ISO_FULL.map(v => [String(v), v]), cur, pick);
chips(sh, ISO_ALL.filter(v => !ISO_FULL.includes(v)).map(v => [String(v), v]), cur, pick, new Set(ISO_ALL));
sh.appendChild(h("div", "note", "Nativos de la Pocket 4K: 400 y 3200 · tercios en gris"));
} else if (kind === "shutter") {
head(sh, "OBTURADOR", S.shutter || "—");
const curA = S.angle, curS = S.speed;
sh.appendChild(h("div", "sub", "ÁNGULO"));
const pa = a => { cmd("angle", { a }); closeSheet(); };
chips(sh, ANG_MAIN.map(a => [a + "°", a]), curA, pa);
chips(sh, ANG_ALL.filter(a => !ANG_MAIN.includes(a)).map(a => [a + "°", a]), curA, pa, new Set(ANG_ALL));
sh.appendChild(h("div", "sub", "VELOCIDAD 1/x"));
chips(sh, SPEEDS.map(s => ["1/" + s, s]), curS, s => { cmd("speed", { s }); closeSheet(); });
} else if (kind === "iris") {
head(sh, "IRIS", S.iris || "—");
chips(sh, FSTOPS.map(f => ["f/" + f, f]), null, f => { cmd("iris_f", { f }); closeSheet(); });
const kv = h("div", "kv"); kv.appendChild(h("span", "", "abierto"));
const r = h("input"); r.type = "range"; r.min = 0; r.max = 1000; r.value = Math.round((S.aperture || 0) * 1000);
r.oninput = () => { dragging = "iris"; throttled("iris", () => cmd("iris", { v: r.value / 1000 })); };
r.onchange = () => { cmd("iris", { v: r.value / 1000 }); setTimeout(() => dragging = null, 300); };
kv.appendChild(r); kv.appendChild(h("span", "", "cerrado")); sh.appendChild(kv);
const g = h("div", "chips"); const a = h("div", "chip", "AUTO IRIS"); a.onclick = () => { cmd("auto_iris"); closeSheet(); }; g.appendChild(a); sh.appendChild(g);
} else if (kind === "wb") {
const cv = head(sh, "BALANCE DE BLANCOS", "");
const refresh = () => cv.textContent = `${wbState.k}K ${wbState.t >= 0 ? "+" : ""}${wbState.t}`;
const send = () => throttled("wb", () => cmd("wb", { k: wbState.k, t: wbState.t }));
refresh();
const kv = h("div", "kv"); kv.appendChild(h("span", "", "2500"));
const ks = h("input"); ks.type = "range"; ks.min = 2500; ks.max = 10000; ks.step = 50; ks.value = wbState.k;
ks.oninput = () => { wbState.k = +ks.value; refresh(); send(); markPreset(); };
kv.appendChild(ks); kv.appendChild(h("span", "", "10000")); sh.appendChild(kv);
const ticks = h("div", "ticks"); for (const k of [3200, 4000, 5600, 6500, 7500]) ticks.appendChild(h("span", "", String(k))); sh.appendChild(ticks);
const g = h("div", "chips"); g.style.gridTemplateColumns = "repeat(3, 1fr)";
const presetChips = [];
for (const [label, k, t] of WB_CHIPS) {
const c = h("div", "chip", label); c.dataset.k = k;
c.onclick = () => { wbState.k = k; wbState.t = t; ks.value = k; ts.value = t; refresh(); send(); markPreset(); };
g.appendChild(c); presetChips.push(c);
}
const awb = h("div", "chip", "AUTO WB"); awb.onclick = () => { cmd("awb"); closeSheet(); }; g.appendChild(awb);
sh.appendChild(g);
const markPreset = () => presetChips.forEach(c => c.classList.toggle("sel", +c.dataset.k === wbState.k));
const tr = h("div", "kv"); tr.style.gridTemplateColumns = "36px 1fr 30px 30px";
tr.appendChild(h("span", "", "Tinte"));
const ts = h("input"); ts.type = "range"; ts.min = -50; ts.max = 50; ts.value = wbState.t;
ts.oninput = () => { wbState.t = +ts.value; refresh(); send(); };
tr.appendChild(ts);
for (const [txt, d] of [["−", -1], ["+", 1]]) { const b = h("button", "sq", txt); b.onclick = () => { wbState.t = Math.max(-50, Math.min(50, wbState.t + d)); ts.value = wbState.t; refresh(); send(); }; tr.appendChild(b); }
sh.appendChild(tr);
markPreset();
} else if (kind === "peak") {
head(sh, "COLOR DEL PEAKING", PEAK_NAME[S.peak_colour || 0]);
const g = h("div", "chips");
PEAK_NAME.forEach((n, i) => { const c = h("div", "chip" + (i === (S.peak_colour || 0) ? " sel" : "")); c.innerHTML = `<span class="swatch" style="background:${PEAK_COL[i]}"></span>${n}`; c.onclick = () => { cmd("peak_colour", { c: i }); closeSheet(); }; g.appendChild(c); });
sh.appendChild(g);
sh.appendChild(h("div", "note", "Clic derecho en PEAK abre este selector"));
}
}
})();
</script>
</body>
</html>