{"id":2,"date":"2026-01-07T05:11:02","date_gmt":"2026-01-07T05:11:02","guid":{"rendered":"http:\/\/192.168.50.237\/?page_id=2"},"modified":"2026-06-26T17:28:32","modified_gmt":"2026-06-26T17:28:32","slug":"home","status":"publish","type":"page","link":"https:\/\/darkhartlabs.com\/","title":{"rendered":"Hart Buddies"},"content":{"rendered":"    <div id=\"hart-buddies-demo\" class=\"hbd-wrap\">\n        <section class=\"hbd-panel hbd-hero\">\n            <div>\n                <p class=\"hbd-kicker\">Hart Buddies demo<\/p>\n                <h1>Generate a character<\/h1>\n                <p class=\"hbd-subtitle\">The demo runs on darkhartlabs.com. Packout handles the model call.<\/p>\n            <\/div>\n            <div class=\"hbd-actions\">\n                <label class=\"hbd-model-label\" for=\"hbd-model\">Model<\/label>\n                <select id=\"hbd-model\" class=\"hbd-model\">\n                                            <option value=\"gpt-oss:20b\" >gpt-oss:20b<\/option>\n                                            <option value=\"gpt-oss:120b\" >gpt-oss:120b<\/option>\n                                            <option value=\"ministral-3:8b-instruct-2512-q4_K_M\" >ministral-3:8b-instruct-2512-q4_K_M<\/option>\n                                            <option value=\"ministral-3:8b-instruct-2512-q4_K_M-32k\" >ministral-3:8b-instruct-2512-q4_K_M-32k<\/option>\n                                            <option value=\"olmo-3:7b\" >olmo-3:7b<\/option>\n                                            <option value=\"olmo-3:7b-32k\" >olmo-3:7b-32k<\/option>\n                                            <option value=\"Packout20-64k:latest\"  selected='selected'>Packout20-64k:latest<\/option>\n                                            <option value=\"Packout20-128k:latest\" >Packout20-128k:latest<\/option>\n                                            <option value=\"Packout120-64k:latest\" >Packout120-64k:latest<\/option>\n                                            <option value=\"Packout120-128k:latest\" >Packout120-128k:latest<\/option>\n                                            <option value=\"PackoutQ3_Next-256k:latest\" >PackoutQ3_Next-256k:latest<\/option>\n                                            <option value=\"qwen3-coder-next:q4_K_M\" >qwen3-coder-next:q4_K_M<\/option>\n                                            <option value=\"qwen3-embedding:8b\" >qwen3-embedding:8b<\/option>\n                                            <option value=\"qwen3-embedding:8b-fp16\" >qwen3-embedding:8b-fp16<\/option>\n                                            <option value=\"qwen3-next:80b-a3b-thinking-q4_K_M\" >qwen3-next:80b-a3b-thinking-q4_K_M<\/option>\n                                            <option value=\"qwen3-vl:30b-a3b-instruct-bf16\" >qwen3-vl:30b-a3b-instruct-bf16<\/option>\n                                    <\/select>\n                <button id=\"hbd-generate\" type=\"button\">Generate<\/button>\n            <\/div>\n        <\/section>\n\n        <section id=\"hbd-loading\" class=\"hbd-panel hbd-loading\" hidden>\n            <div class=\"hbd-loading-top\">\n                <strong id=\"hbd-status\">Starting...<\/strong>\n                <span id=\"hbd-elapsed\">0s<\/span>\n            <\/div>\n            <div class=\"hbd-bar\"><div class=\"hbd-bar-fill\"><\/div><\/div>\n        <\/section>\n\n        <section class=\"hbd-panel\">\n            <div id=\"hbd-result\" class=\"hbd-result hbd-empty\">Click Generate to create one Hart Buddies character.<\/div>\n        <\/section>\n        <p class=\"hbd-index-link\"><a href=\"https:\/\/darkhartlabs.com\/?page_id=126\">View generated character index<\/a> <span aria-hidden=\"true\">\/<\/span> <a href=\"https:\/\/darkhartlabs.com\/?page_id=127\">View generator parameters<\/a><\/p>\n    <\/div>\n\n    <style>\n        .hbd-wrap { max-width: 1120px; margin: 0 auto; padding: 18px; color: #111814; }\n        .hbd-panel { background: #ffffff; border: 1px solid #d8ded7; border-radius: 8px; padding: 18px; margin: 0 0 14px; box-shadow: 0 10px 24px rgba(31,42,36,.08); }\n        .hbd-hero { display: flex; justify-content: space-between; align-items: center; gap: 18px; border-top: 4px solid #c7822d; }\n        .hbd-kicker { margin: 0 0 4px; color: #26352d; font-weight: 800; text-transform: uppercase; font-size: 13px; letter-spacing: .03em; }\n        .hbd-hero h1 { margin: 0; color: #0f1713; font-size: clamp(30px, 4vw, 52px); line-height: 1.02; }\n        .hbd-subtitle { margin: 8px 0 0; color: #243229; }\n        .hbd-actions { display: grid; grid-template-columns: auto auto; align-items: end; gap: 8px 10px; }\n        .hbd-model-label { grid-column: 1 \/ -1; color: #243229; font-size: 12px; font-weight: 900; text-transform: uppercase; }\n        .hbd-model { min-width: 270px; max-width: 100%; border: 1px solid #c8d4cd; border-radius: 6px; background: #fff; color: #111814; padding: 11px 12px; font-weight: 700; }\n        .hbd-model:disabled { opacity: .65; cursor: not-allowed; }\n        #hbd-generate { border: 0; border-radius: 6px; background: #2d6d55; color: #fff; padding: 12px 20px; font-weight: 800; cursor: pointer; min-width: 130px; }\n        #hbd-generate:hover { background: #1e503d; }\n        #hbd-generate:disabled { opacity: .6; cursor: not-allowed; }\n        .hbd-loading-top { display: flex; justify-content: space-between; gap: 12px; color: #16221b; }\n        .hbd-bar { height: 18px; border-radius: 999px; overflow: hidden; border: 1px solid #c8d4cd; background: #e8eee9; margin-top: 12px; }\n        .hbd-bar-fill { height: 100%; width: 42%; background: repeating-linear-gradient(45deg, #2d6d55, #2d6d55 12px, #3f876c 12px, #3f876c 24px); animation: hbd-slide 1s linear infinite; }\n        @keyframes hbd-slide { 0% { transform: translateX(-55%); } 100% { transform: translateX(240%); } }\n        .hbd-empty { color: #243229; border: 1px dashed #d8ded7; border-radius: 8px; background: #fbfcfa; padding: 20px; }\n        .hbd-index-link { margin: 4px 0 0; text-align: right; font-weight: 800; }\n        .hbd-index-link a { color: #e8f4ec !important; }\n        .hbd-index-link a:hover, .hbd-index-link a:focus { color: #78d6ff !important; }\n        .hbd-error { white-space: pre-wrap; color: #8f2f28; border: 1px solid #e2b7b1; background: #fff7f5; border-radius: 8px; padding: 14px; }\n        .hbd-section { border: 1px solid #d8ded7; border-radius: 8px; overflow: hidden; margin: 0 0 12px; background: #fbfcfa; }\n        .hbd-section h2 { margin: 0; padding: 12px 14px; color: #0f1713; font-size: 18px; background: #edf2ee; border-bottom: 1px solid #d8ded7; }\n        .hbd-section-body { padding: 14px; display: grid; gap: 10px; }\n        .hbd-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }\n        .hbd-field { background: #fff; border: 1px solid #e0e6e1; border-radius: 6px; padding: 10px; min-width: 0; }\n        .hbd-key { display: block; color: #243229; font-size: 12px; font-weight: 900; text-transform: uppercase; margin-bottom: 5px; }\n        .hbd-value { white-space: pre-wrap; overflow-wrap: anywhere; }\n        .hbd-field ul { margin: 0; padding-left: 20px; }\n        .hbd-character-image { display: block; width: min(100%, 512px); height: auto; border-radius: 8px; border: 1px solid #d8ded7; background: #111814; }\n        .hbd-raw summary { cursor: pointer; font-weight: 800; }\n        .hbd-raw pre { white-space: pre-wrap; overflow: auto; max-height: 420px; background: #1f2a24; color: #e8f4ec; border-radius: 6px; padding: 12px; font-size: 12px; }\n        @media (max-width: 760px) { .hbd-hero { align-items: stretch; flex-direction: column; } .hbd-actions { grid-template-columns: 1fr; } .hbd-model { width: 100%; min-width: 0; } #hbd-generate { width: 100%; } .hbd-grid { grid-template-columns: 1fr; } }\n    <\/style>\n\n    <script>\n    (() => {\n        const startUrl = \"https:\\\/\\\/darkhartlabs.com\\\/index.php?rest_route=\\\/hart-buddies\\\/v1\\\/start\";\n        const jobUrl = \"https:\\\/\\\/darkhartlabs.com\\\/index.php?rest_route=\\\/hart-buddies\\\/v1\\\/job\";\n        const button = document.getElementById('hbd-generate');\n        const model = document.getElementById('hbd-model');\n        const loading = document.getElementById('hbd-loading');\n        const status = document.getElementById('hbd-status');\n        const elapsed = document.getElementById('hbd-elapsed');\n        const result = document.getElementById('hbd-result');\n        let timer = null;\n        let started = 0;\n        let poller = null;\n\n        const esc = (value) => String(value ?? '').replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('\"', '&quot;').replaceAll(\"'\", '&#039;');\n        const nice = (key) => String(key).replaceAll('_', ' ').replace(\/\\b\\w\/g, c => c.toUpperCase());\n\n        function startClock() {\n            started = Date.now();\n            loading.hidden = false;\n            timer = setInterval(() => {\n                const seconds = Math.floor((Date.now() - started) \/ 1000);\n                const minutes = Math.floor(seconds \/ 60);\n                const rest = seconds % 60;\n                elapsed.textContent = minutes ? `${minutes}m ${rest}s` : `${seconds}s`;\n            }, 1000);\n        }\n\n        function stopClock() {\n            if (timer) clearInterval(timer);\n            if (poller) clearInterval(poller);\n            timer = null;\n            poller = null;\n            loading.hidden = true;\n            button.disabled = false;\n            if (model) model.disabled = false;\n        }\n\n        function renderInline(value) {\n            if (value && typeof value === 'object') return esc(JSON.stringify(value));\n            if (typeof value === 'boolean') return value ? 'Yes' : 'No';\n            return esc(value);\n        }\n\n        function renderValue(value) {\n            if (Array.isArray(value)) {\n                if (!value.length) return '<div class=\"hbd-value\">None<\/div>';\n                return `<ul>${value.map(item => `<li>${renderInline(item)}<\/li>`).join('')}<\/ul>`;\n            }\n            if (value && typeof value === 'object') {\n                return `<div class=\"hbd-grid\">${Object.entries(value).map(([k, v]) => renderField(k, v)).join('')}<\/div>`;\n            }\n            if (typeof value === 'boolean') return `<div class=\"hbd-value\">${value ? 'Yes' : 'No'}<\/div>`;\n            return `<div class=\"hbd-value\">${esc(value)}<\/div>`;\n        }\n\n        function renderField(key, value) {\n            return `<div class=\"hbd-field\"><span class=\"hbd-key\">${esc(nice(key))}<\/span>${renderValue(value)}<\/div>`;\n        }\n\n        function renderSection(key, value) {\n            return `<div class=\"hbd-section\"><h2>${esc(nice(key))}<\/h2><div class=\"hbd-section-body\">${renderValue(value)}<\/div><\/div>`;\n        }\n\n        function renderStableDiffusionPrompt(sdPrompt) {\n            if (!sdPrompt) return '';\n            if (sdPrompt.status === 'failed') {\n                return `<div class=\"hbd-section\"><h2>Stable Diffusion Prompt<\/h2><div class=\"hbd-section-body\"><div class=\"hbd-error\">${esc(sdPrompt.error || sdPrompt.message || 'Stable Diffusion prompt generation failed.')}<\/div><\/div><\/div>`;\n            }\n            return `<div class=\"hbd-section\"><h2>Stable Diffusion Prompt<\/h2><div class=\"hbd-section-body\">\n                <div class=\"hbd-field\"><span class=\"hbd-key\">Model<\/span><div class=\"hbd-value\">${esc(sdPrompt.model || '')}<\/div><\/div>\n                <div class=\"hbd-field\"><span class=\"hbd-key\">Prompt<\/span><div class=\"hbd-value\">${esc(sdPrompt.prompt || '')}<\/div><\/div>\n                ${sdPrompt.negative_prompt ? `<div class=\"hbd-field\"><span class=\"hbd-key\">Negative Prompt<\/span><div class=\"hbd-value\">${esc(sdPrompt.negative_prompt)}<\/div><\/div>` : ''}\n                ${sdPrompt.notes ? `<div class=\"hbd-field\"><span class=\"hbd-key\">Notes<\/span><div class=\"hbd-value\">${esc(sdPrompt.notes)}<\/div><\/div>` : ''}\n            <\/div><\/div>`;\n        }\n\n        function renderStableDiffusionImage(sdImage) {\n            if (!sdImage) return '';\n            if (sdImage.status === 'failed') {\n                return `<div class=\"hbd-section\"><h2>Stable Diffusion Image<\/h2><div class=\"hbd-section-body\"><div class=\"hbd-error\">${esc(sdImage.error || sdImage.message || 'Stable Diffusion image generation failed.')}<\/div><\/div><\/div>`;\n            }\n            if (!sdImage.url) return '';\n            return `<div class=\"hbd-section\"><h2>Stable Diffusion Image<\/h2><div class=\"hbd-section-body\">\n                <img decoding=\"async\" class=\"hbd-character-image\" src=\"${esc(sdImage.url)}\" alt=\"${esc(sdImage.filename || 'Generated character image')}\" loading=\"lazy\">\n                <div class=\"hbd-grid\">\n                    <div class=\"hbd-field\"><span class=\"hbd-key\">Sampler<\/span><div class=\"hbd-value\">${esc(sdImage.sampler_name || '')}<\/div><\/div>\n                    <div class=\"hbd-field\"><span class=\"hbd-key\">Seed<\/span><div class=\"hbd-value\">${esc(sdImage.seed ?? '')}<\/div><\/div>\n                    <div class=\"hbd-field\"><span class=\"hbd-key\">Size<\/span><div class=\"hbd-value\">${esc(sdImage.width || '')} x ${esc(sdImage.height || '')}<\/div><\/div>\n                    <div class=\"hbd-field\"><span class=\"hbd-key\">Steps<\/span><div class=\"hbd-value\">${esc(sdImage.steps || '')}<\/div><\/div>\n                <\/div>\n            <\/div><\/div>`;\n        }\n\n        function renderComplete(job) {\n            const parsed = job.parsed;\n            if (!parsed) {\n                result.className = 'hbd-result';\n                result.innerHTML = `<div class=\"hbd-error\">Packout returned text, but it was not parseable JSON. Raw response is below.<\/div><details class=\"hbd-raw\" open><summary>Raw response<\/summary><pre>${esc(job.raw_response || '')}<\/pre><\/details>`;\n                return;\n            }\n            const name = parsed.selected_name?.display_name || parsed.selected_name?.name || 'Generated Character';\n            const tagline = parsed.selected_name?.short_tagline || '';\n            result.className = 'hbd-result';\n            result.innerHTML = `\n                <div class=\"hbd-section\"><h2>${esc(name)}<\/h2><div class=\"hbd-section-body\">\n                    <div class=\"hbd-field\"><span class=\"hbd-key\">Tagline<\/span><div class=\"hbd-value\">${esc(tagline)}<\/div><\/div>\n                    <div class=\"hbd-field\"><span class=\"hbd-key\">Model<\/span><div class=\"hbd-value\">${esc(job.packout_model || '')}<\/div><\/div>\n                    <div class=\"hbd-field\"><span class=\"hbd-key\">Stable Diffusion Prompt Model<\/span><div class=\"hbd-value\">${esc(job.stable_diffusion_prompt?.model || '')}<\/div><\/div>\n                    <div class=\"hbd-field\"><span class=\"hbd-key\">Elapsed<\/span><div class=\"hbd-value\">${Number(job.elapsed_seconds || 0).toFixed(1)} seconds<\/div><\/div>\n                <\/div><\/div>\n                ${renderStableDiffusionImage(job.stable_diffusion_image)}\n                ${renderStableDiffusionPrompt(job.stable_diffusion_prompt)}\n                ${Object.entries(parsed).map(([k, v]) => renderSection(k, v)).join('')}\n                <details class=\"hbd-raw\"><summary>Raw JSON<\/summary><pre>${esc(JSON.stringify(parsed, null, 2))}<\/pre><\/details>`;\n        }\n\n        async function getJson(url, options = {}) {\n            const response = await fetch(url, options);\n            const text = await response.text();\n            let payload;\n            try { payload = JSON.parse(text); } catch (e) { throw new Error(text || `HTTP ${response.status}`); }\n            if (!response.ok || payload.code) throw new Error(payload.message || `HTTP ${response.status}`);\n            return payload;\n        }\n\n        function jobPollUrl(jobId) {\n            const url = new URL(jobUrl);\n            url.searchParams.set('job_id', jobId);\n            url.searchParams.set('t', Date.now());\n            return url.toString();\n        }\n\n        async function poll(jobId) {\n            const job = await getJson(jobPollUrl(jobId));\n            status.textContent = job.message || job.status || 'Working...';\n            if (job.status === 'complete') {\n                stopClock();\n                status.textContent = 'Done.';\n                renderComplete(job);\n            } else if (job.status === 'failed') {\n                stopClock();\n                result.className = 'hbd-result';\n                result.innerHTML = `<div class=\"hbd-error\">${esc(job.error || job.message || 'Generation failed.')}<\/div>`;\n            }\n        }\n\n        button.addEventListener('click', async () => {\n            button.disabled = true;\n            if (model) model.disabled = true;\n            const selectedModel = model ? model.value : '';\n            result.className = 'hbd-result hbd-empty';\n            result.textContent = selectedModel ? `Waiting for Packout using ${selectedModel}...` : 'Waiting for Packout...';\n            status.textContent = 'Starting generation job...';\n            startClock();\n            try {\n                const startedJob = await getJson(startUrl, {\n                    method: 'POST',\n                    headers: {'Content-Type': 'application\/json'},\n                    body: JSON.stringify({model: selectedModel}),\n                });\n                status.textContent = `Generation job started with ${startedJob.packout_model || selectedModel}.`;\n                poller = setInterval(() => poll(startedJob.job_id).catch(error => {\n                    stopClock();\n                    result.className = 'hbd-result';\n                    result.innerHTML = `<div class=\"hbd-error\">${esc(error.message)}<\/div>`;\n                }), 2500);\n                await poll(startedJob.job_id);\n            } catch (error) {\n                stopClock();\n                result.className = 'hbd-result';\n                result.innerHTML = `<div class=\"hbd-error\">${esc(error.message)}<\/div>`;\n            }\n        });\n    })();\n    <\/script>\n    \n","protected":false},"excerpt":{"rendered":"","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"open","template":"","meta":{"footnotes":""},"class_list":["post-2","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/darkhartlabs.com\/index.php?rest_route=\/wp\/v2\/pages\/2","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/darkhartlabs.com\/index.php?rest_route=\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/darkhartlabs.com\/index.php?rest_route=\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/darkhartlabs.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/darkhartlabs.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=2"}],"version-history":[{"count":8,"href":"https:\/\/darkhartlabs.com\/index.php?rest_route=\/wp\/v2\/pages\/2\/revisions"}],"predecessor-version":[{"id":123,"href":"https:\/\/darkhartlabs.com\/index.php?rest_route=\/wp\/v2\/pages\/2\/revisions\/123"}],"wp:attachment":[{"href":"https:\/\/darkhartlabs.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=2"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}