refacil-sdd-ai 5.2.2 → 5.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE.md +46 -0
- package/README.md +209 -42
- package/agents/auditor.md +46 -0
- package/agents/debugger.md +41 -1
- package/agents/implementer.md +76 -10
- package/agents/investigator.md +36 -0
- package/agents/proposer.md +46 -2
- package/agents/tester.md +45 -8
- package/agents/validator.md +67 -13
- package/bin/cli.js +428 -83
- package/bin/postinstall.js +20 -0
- package/lib/bus/broker.js +121 -3
- package/lib/bus/spawn.js +189 -121
- package/lib/check-review.js +102 -0
- package/lib/codegraph-telemetry.js +135 -0
- package/lib/codegraph.js +273 -0
- package/lib/commands/autopilot.js +120 -0
- package/lib/commands/bus.js +29 -36
- package/lib/commands/compact.js +185 -46
- package/lib/commands/read-spec.js +352 -0
- package/lib/commands/sdd.js +429 -44
- package/lib/compact-guidance.js +122 -77
- package/lib/config.js +136 -0
- package/lib/global-paths.js +56 -20
- package/lib/hooks.js +32 -4
- package/lib/ide-detection.js +1 -1
- package/lib/ignore-files.js +5 -1
- package/lib/installer.js +202 -19
- package/lib/kapso.js +241 -0
- package/lib/methodology-migration-pending.js +13 -0
- package/lib/open-browser.js +32 -0
- package/lib/opencode-migrate.js +148 -0
- package/lib/opencode-plugin/index.js +84 -104
- package/lib/opencode-plugin/rules.js +236 -0
- package/lib/project-root.js +154 -0
- package/lib/repo-ide-sync.js +5 -0
- package/lib/spec-reader/lang.js +72 -0
- package/lib/spec-reader/md-parser.js +299 -0
- package/lib/spec-reader/session.js +139 -0
- package/lib/spec-reader/ui/app.js +685 -0
- package/lib/spec-reader/ui/index.html +59 -0
- package/lib/spec-reader/ui/mixed-lang.js +200 -0
- package/lib/spec-reader/ui/model-cache.js +117 -0
- package/lib/spec-reader/ui/style.css +294 -0
- package/lib/spec-reader/ui/supertonic-helper.js +565 -0
- package/lib/spec-sync.js +258 -0
- package/lib/test-scope.js +713 -0
- package/lib/testing-policy-sync.js +14 -2
- package/package.json +6 -3
- package/skills/apply/SKILL.md +39 -64
- package/skills/archive/SKILL.md +74 -48
- package/skills/ask/SKILL.md +43 -8
- package/skills/autopilot/SKILL.md +476 -0
- package/skills/bug/SKILL.md +52 -53
- package/skills/explore/SKILL.md +48 -1
- package/skills/guide/SKILL.md +31 -13
- package/skills/inbox/SKILL.md +9 -0
- package/skills/join/SKILL.md +1 -1
- package/skills/prereqs/BUS-CROSS-REPO.md +33 -16
- package/skills/prereqs/METHODOLOGY-CONTRACT.md +96 -17
- package/skills/prereqs/SKILL.md +1 -1
- package/skills/propose/SKILL.md +74 -19
- package/skills/read-spec/SKILL.md +76 -0
- package/skills/reply/SKILL.md +42 -9
- package/skills/review/SKILL.md +63 -25
- package/skills/review/checklist.md +2 -2
- package/skills/say/SKILL.md +40 -4
- package/skills/setup/SKILL.md +59 -5
- package/skills/setup/troubleshooting.md +11 -3
- package/skills/stats/SKILL.md +157 -0
- package/skills/test/SKILL.md +35 -10
- package/skills/up-code/SKILL.md +20 -13
- package/skills/update/SKILL.md +32 -1
- package/skills/verify/SKILL.md +78 -41
- package/templates/compact-guidance.md +10 -0
- package/templates/methodology-guide.md +5 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>refacil read-spec — TTS</title>
|
|
7
|
+
<link rel="stylesheet" href="/read-spec/style.css" />
|
|
8
|
+
<script type="importmap">
|
|
9
|
+
{
|
|
10
|
+
"imports": {
|
|
11
|
+
"onnxruntime-web": "https://cdn.jsdelivr.net/npm/onnxruntime-web@1.22.0/dist/ort.min.mjs"
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
</script>
|
|
15
|
+
<script src="https://cdn.jsdelivr.net/npm/marked@15.0.4/marked.min.js" integrity="sha384-2NndlFAl0twi2nmu3w9mixodZFiN2fbxlakcYJF86bn1JeS1VB87/XqA7cqymzS+" crossorigin="anonymous"></script>
|
|
16
|
+
</head>
|
|
17
|
+
<body>
|
|
18
|
+
<header>
|
|
19
|
+
<h1>refacil read-spec</h1>
|
|
20
|
+
<p class="meta" id="source-meta">Loading session…</p>
|
|
21
|
+
</header>
|
|
22
|
+
<p id="status-bar">Preparing…</p>
|
|
23
|
+
<div class="layout">
|
|
24
|
+
<nav id="file-sidebar" class="file-sidebar hidden" aria-label="File navigation"></nav>
|
|
25
|
+
<main id="sections"></main>
|
|
26
|
+
<aside id="controls">
|
|
27
|
+
<div class="control-row">
|
|
28
|
+
<button type="button" id="prev-btn" disabled>Previous</button>
|
|
29
|
+
<button type="button" id="play-btn" disabled>Play</button>
|
|
30
|
+
<button type="button" id="next-btn" disabled>Next</button>
|
|
31
|
+
</div>
|
|
32
|
+
<label>
|
|
33
|
+
Language
|
|
34
|
+
<select id="lang-select"></select>
|
|
35
|
+
</label>
|
|
36
|
+
<label>
|
|
37
|
+
Voice
|
|
38
|
+
<select id="voice-select">
|
|
39
|
+
<option value="M1">M1</option>
|
|
40
|
+
<option value="M2">M2</option>
|
|
41
|
+
<option value="M3" selected>M3</option>
|
|
42
|
+
<option value="M4">M4</option>
|
|
43
|
+
<option value="M5">M5</option>
|
|
44
|
+
<option value="F1">F1</option>
|
|
45
|
+
<option value="F2">F2</option>
|
|
46
|
+
<option value="F3">F3</option>
|
|
47
|
+
<option value="F4">F4</option>
|
|
48
|
+
<option value="F5">F5</option>
|
|
49
|
+
</select>
|
|
50
|
+
</label>
|
|
51
|
+
<label>
|
|
52
|
+
Speed <span id="speed-val">1</span>
|
|
53
|
+
<input type="range" id="speed-range" min="0.9" max="1.5" step="0.05" value="1" />
|
|
54
|
+
</label>
|
|
55
|
+
</aside>
|
|
56
|
+
</div>
|
|
57
|
+
<script type="module" src="/read-spec/app.js"></script>
|
|
58
|
+
</body>
|
|
59
|
+
</html>
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Split Spanish spec text into TTS segments so English terms (paths, flags, code)
|
|
3
|
+
* are synthesized with lang=en and the rest with lang=es.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** @typedef {{ lang: string, text: string }} LangSegment */
|
|
7
|
+
|
|
8
|
+
/** Pausa mínima recomendada entre trozos es/en al concatenar WAV (segundos). */
|
|
9
|
+
export const MIXED_LANG_GAP_SEC = 0.015;
|
|
10
|
+
|
|
11
|
+
/** Máximo de inferencias TTS por sección (cada una ~varios segundos en el navegador). */
|
|
12
|
+
export const MAX_TTS_SEGMENTS = 16;
|
|
13
|
+
|
|
14
|
+
/** Case-sensitive patterns only (no /i) to avoid marking Spanish words as English. */
|
|
15
|
+
const ENGLISH_SPAN_RE = new RegExp(
|
|
16
|
+
[
|
|
17
|
+
'code block:\\s*\\w+',
|
|
18
|
+
'\\brefacil-sdd-ai(?:\\s+(?:read-spec|sync-spec|bus|archive|propose|verify|review|test|apply|help|config|--[\\w-]+|(?:[\\w@]+/)+[\\w./-]+|[\\w@][\\w./-]*\\.[a-z]{2,5}))*\\b',
|
|
19
|
+
'/refacil:[\\w-]+',
|
|
20
|
+
'--[\\w-]+',
|
|
21
|
+
'\\brefacil-[\\w-]+\\b',
|
|
22
|
+
'(?:\\b[\\w@]+/)+[\\w./-]+',
|
|
23
|
+
'\\b[\\w@][\\w./-]*\\.[a-z]{2,5}\\b',
|
|
24
|
+
// Uppercase acronyms: HTML, CSS, API, TTS, CDN, SRI, REST, DOM, JSON, CI, CD, PR, etc.
|
|
25
|
+
'\\b[A-Z]{2,}(?:[.-][A-Z0-9]+)*\\b',
|
|
26
|
+
'\\b(?:endpoint|payload|token|broker|queue|webhook|session|request|response|handler|middleware|callback|schema|query|buffer|stream|chunk|pipeline|runtime|deploy|build|merge|branch|commit|staging|timeout|mock|stub|parser|router|store|state|action|backend|frontend|gateway|socket|worker|event|hook|plugin|cache|blob|lambda|trigger|scaffold|view|model|repository|migration|seed|rollback|snapshot|diff|patch|header|cookie|cors|csrf|jwt|oauth|scoped|typed|mixin|decorator|hydrat)(?:e|s|es|ed|ing|er)?\\b',
|
|
27
|
+
// Common English tech words that appear in Spanish SDD specs
|
|
28
|
+
'\\b(?:scope|render|parse|fetch|input|output|format|sanitize|validate|serialize|deserialize|encode|decode|import|export|layout|sidebar|fallback|checkbox|dropdown|tooltip|toggle|badge|overlay|modal|toast|banner|loader|spinner|inline|bundle|chunk|wrapper|container|grid|flex|offset|margin|padding|breakpoint|viewport|hover|focus|click|submit|reset|drag|drop|resize|scroll|swipe|tab|panel|frame|slot|portal|fixture|snapshot|coverage|lint|typecheck|watch|serve|preview|release|tag|label|badge|draft|issue|ticket|pr|branch|rebase|squash|stash|worktree|submodule|monorepo|workspace|package|registry|artifact|manifest|lockfile|polyfill|shim|transpile|minify|tree.shak|tree-shak)(?:e|s|es|ed|ing|er)?\\b',
|
|
29
|
+
'\\b[a-z][a-z0-9]*(?:_[a-z][a-z0-9]*)+\\b',
|
|
30
|
+
'\\b[a-z]{2,}(?:-[a-z0-9]+)+\\b',
|
|
31
|
+
'\\b[a-z]+(?:[A-Z][a-z0-9]*)+\\b',
|
|
32
|
+
'\\b(?:IndexedDB|HuggingFace|Supertonic|onnxruntime(?:-web)?|Markdown|onnxruntime-web|Node\\.js|Human-in-the-Loop)\\b',
|
|
33
|
+
].join('|'),
|
|
34
|
+
'g',
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param {Float32Array|number[]} wav
|
|
39
|
+
* @param {number} sampleRate
|
|
40
|
+
* @param {number} [threshold]
|
|
41
|
+
* @returns {number[]}
|
|
42
|
+
*/
|
|
43
|
+
export function trimTrailingSilence(wav, sampleRate, threshold = 0.006) {
|
|
44
|
+
const minTail = Math.floor(sampleRate * 0.02);
|
|
45
|
+
let end = wav.length;
|
|
46
|
+
while (end > minTail && Math.abs(wav[end - 1]) < threshold) {
|
|
47
|
+
end -= 1;
|
|
48
|
+
}
|
|
49
|
+
return wav.slice(0, end);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @param {Float32Array|number[]} wav
|
|
54
|
+
* @param {number} [threshold]
|
|
55
|
+
* @returns {number[]}
|
|
56
|
+
*/
|
|
57
|
+
export function trimLeadingSilence(wav, threshold = 0.006) {
|
|
58
|
+
let start = 0;
|
|
59
|
+
while (start < wav.length && Math.abs(wav[start]) < threshold) {
|
|
60
|
+
start += 1;
|
|
61
|
+
}
|
|
62
|
+
return wav.slice(start);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @param {LangSegment[]} segments
|
|
67
|
+
* @returns {LangSegment[]}
|
|
68
|
+
*/
|
|
69
|
+
function mergeAdjacentSegments(segments) {
|
|
70
|
+
/** @type {LangSegment[]} */
|
|
71
|
+
const out = [];
|
|
72
|
+
for (const seg of segments) {
|
|
73
|
+
const text = (seg.text || '').replace(/\s+/g, ' ').trim();
|
|
74
|
+
if (!text) continue;
|
|
75
|
+
const prev = out[out.length - 1];
|
|
76
|
+
if (prev && prev.lang === seg.lang) {
|
|
77
|
+
prev.text = `${prev.text} ${text}`;
|
|
78
|
+
} else {
|
|
79
|
+
out.push({ lang: seg.lang, text });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Minimum chars for a Spanish segment to stand alone; below this it is punctuation/conjunction. */
|
|
86
|
+
const MIN_SEGMENT_CHARS = 4;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Merge short Spanish segments (punctuation, conjunctions) into the previous segment
|
|
90
|
+
* to avoid generating a separate TTS inference — and a perceptible pause — for single commas
|
|
91
|
+
* or short connectors sandwiched between English spans.
|
|
92
|
+
* @param {LangSegment[]} segments
|
|
93
|
+
* @returns {LangSegment[]}
|
|
94
|
+
*/
|
|
95
|
+
function mergeShortSegments(segments) {
|
|
96
|
+
const out = [];
|
|
97
|
+
for (const seg of segments) {
|
|
98
|
+
const text = (seg.text || '').replace(/\s+/g, ' ').trim();
|
|
99
|
+
if (!text) continue;
|
|
100
|
+
const prev = out[out.length - 1];
|
|
101
|
+
if (seg.lang === 'es' && text.length <= MIN_SEGMENT_CHARS && prev) {
|
|
102
|
+
prev.text = `${prev.text} ${text}`;
|
|
103
|
+
} else {
|
|
104
|
+
out.push({ lang: seg.lang, text });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return mergeAdjacentSegments(out);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Reduce fragmentación: fusiona pares adyacentes baratos priorizando pares del mismo idioma.
|
|
112
|
+
* @param {LangSegment[]} segments
|
|
113
|
+
* @param {number} [max]
|
|
114
|
+
* @returns {LangSegment[]}
|
|
115
|
+
*/
|
|
116
|
+
export function consolidateSegments(segments, max = MAX_TTS_SEGMENTS) {
|
|
117
|
+
let list = segments.map((s) => ({ lang: s.lang, text: s.text }));
|
|
118
|
+
while (list.length > max) {
|
|
119
|
+
if (list.length < 2) break;
|
|
120
|
+
|
|
121
|
+
// Find the cheapest adjacent pair. Heavy penalty for cross-language merges.
|
|
122
|
+
let bestI = -1;
|
|
123
|
+
let bestScore = Infinity;
|
|
124
|
+
for (let i = 0; i < list.length - 1; i++) {
|
|
125
|
+
const combined = list[i].text.length + list[i + 1].text.length;
|
|
126
|
+
const crossPenalty = list[i].lang !== list[i + 1].lang ? 10000 : 0;
|
|
127
|
+
const score = combined + crossPenalty;
|
|
128
|
+
if (score < bestScore) {
|
|
129
|
+
bestScore = score;
|
|
130
|
+
bestI = i;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const a = list[bestI];
|
|
135
|
+
const b = list[bestI + 1];
|
|
136
|
+
// Same-lang: keep that lang. Cross-lang: keep Spanish (primary doc language).
|
|
137
|
+
const mergedLang = a.lang === b.lang ? a.lang : 'es';
|
|
138
|
+
list[bestI] = { lang: mergedLang, text: `${a.text} ${b.text}`.trim() };
|
|
139
|
+
list.splice(bestI + 1, 1);
|
|
140
|
+
list = mergeAdjacentSegments(list);
|
|
141
|
+
}
|
|
142
|
+
return list;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* @param {number[][]} parts
|
|
147
|
+
* @returns {Float32Array}
|
|
148
|
+
*/
|
|
149
|
+
export function concatWavParts(parts) {
|
|
150
|
+
const totalLen = parts.reduce((n, p) => n + p.length, 0);
|
|
151
|
+
const out = new Float32Array(totalLen);
|
|
152
|
+
let offset = 0;
|
|
153
|
+
for (const part of parts) {
|
|
154
|
+
out.set(part, offset);
|
|
155
|
+
offset += part.length;
|
|
156
|
+
}
|
|
157
|
+
return out;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* @param {string} text
|
|
162
|
+
* @param {string} primaryLang Supertonic code (e.g. es, en)
|
|
163
|
+
* @returns {LangSegment[]}
|
|
164
|
+
*/
|
|
165
|
+
export function splitBilingualText(text, primaryLang) {
|
|
166
|
+
const primary = (primaryLang || 'es').toLowerCase();
|
|
167
|
+
if (!text || primary !== 'es') {
|
|
168
|
+
return [{ lang: primary, text: (text || '').trim() }];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** @type {LangSegment[]} */
|
|
172
|
+
const segments = [];
|
|
173
|
+
let lastIndex = 0;
|
|
174
|
+
const re = new RegExp(ENGLISH_SPAN_RE.source, ENGLISH_SPAN_RE.flags);
|
|
175
|
+
let match;
|
|
176
|
+
while ((match = re.exec(text)) !== null) {
|
|
177
|
+
if (match[0].length === 0) {
|
|
178
|
+
re.lastIndex += 1;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
const before = text.slice(lastIndex, match.index);
|
|
182
|
+
if (before.trim()) {
|
|
183
|
+
segments.push({ lang: 'es', text: before });
|
|
184
|
+
}
|
|
185
|
+
segments.push({ lang: 'en', text: match[0] });
|
|
186
|
+
lastIndex = re.lastIndex;
|
|
187
|
+
}
|
|
188
|
+
const tail = text.slice(lastIndex);
|
|
189
|
+
if (tail.trim()) {
|
|
190
|
+
segments.push({ lang: 'es', text: tail });
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const merged = mergeAdjacentSegments(segments);
|
|
194
|
+
if (!merged.length) {
|
|
195
|
+
return [{ lang: 'es', text: text.trim() }];
|
|
196
|
+
}
|
|
197
|
+
// Absorb short Spanish fragments (punctuation, conjunctions) before counting segments.
|
|
198
|
+
const compacted = mergeShortSegments(merged);
|
|
199
|
+
return consolidateSegments(compacted);
|
|
200
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IndexedDB cache for HuggingFace Supertonic model assets (first download only).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const DB_NAME = 'refacil-read-spec-tts-v1';
|
|
6
|
+
const STORE = 'assets';
|
|
7
|
+
|
|
8
|
+
function openDb() {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
const req = indexedDB.open(DB_NAME, 1);
|
|
11
|
+
req.onerror = () => reject(req.error);
|
|
12
|
+
req.onsuccess = () => resolve(req.result);
|
|
13
|
+
req.onupgradeneeded = () => {
|
|
14
|
+
const db = req.result;
|
|
15
|
+
if (!db.objectStoreNames.contains(STORE)) {
|
|
16
|
+
db.createObjectStore(STORE);
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {string} url
|
|
24
|
+
* @returns {Promise<ArrayBuffer | null>}
|
|
25
|
+
*/
|
|
26
|
+
async function getCached(url) {
|
|
27
|
+
try {
|
|
28
|
+
const db = await openDb();
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
const tx = db.transaction(STORE, 'readonly');
|
|
31
|
+
const req = tx.objectStore(STORE).get(url);
|
|
32
|
+
req.onsuccess = () => resolve(req.result || null);
|
|
33
|
+
req.onerror = () => reject(req.error);
|
|
34
|
+
});
|
|
35
|
+
} catch (_) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {string} url
|
|
42
|
+
* @param {ArrayBuffer} data
|
|
43
|
+
*/
|
|
44
|
+
async function putCached(url, data) {
|
|
45
|
+
try {
|
|
46
|
+
const db = await openDb();
|
|
47
|
+
return new Promise((resolve, reject) => {
|
|
48
|
+
const tx = db.transaction(STORE, 'readwrite');
|
|
49
|
+
tx.objectStore(STORE).put(data, url);
|
|
50
|
+
tx.oncomplete = () => resolve();
|
|
51
|
+
tx.onerror = () => reject(tx.error);
|
|
52
|
+
});
|
|
53
|
+
} catch (_) {
|
|
54
|
+
// ignore cache write failures
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Wrap global fetch to cache Supertone/supertonic-3 assets in IndexedDB.
|
|
60
|
+
* @param {(loaded: number, total: number, label: string) => void} [onProgress]
|
|
61
|
+
*/
|
|
62
|
+
export function installCachedFetch(onProgress) {
|
|
63
|
+
const nativeFetch = window.fetch.bind(window);
|
|
64
|
+
const hfPrefix = 'huggingface.co/Supertone/supertonic-3';
|
|
65
|
+
|
|
66
|
+
window.fetch = async (input, init) => {
|
|
67
|
+
const url = typeof input === 'string' ? input : input.url;
|
|
68
|
+
if (!url.includes(hfPrefix)) {
|
|
69
|
+
return nativeFetch(input, init);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!navigator.onLine) {
|
|
73
|
+
const cached = await getCached(url);
|
|
74
|
+
if (cached) {
|
|
75
|
+
return new Response(cached, { status: 200, headers: { 'content-type': guessContentType(url) } });
|
|
76
|
+
}
|
|
77
|
+
throw new Error('Internet required for first model download. Connect and reload.');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const cached = await getCached(url);
|
|
81
|
+
if (cached) {
|
|
82
|
+
if (onProgress) onProgress(1, 1, url.split('/').pop());
|
|
83
|
+
return new Response(cached, { status: 200, headers: { 'content-type': guessContentType(url) } });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const res = await nativeFetch(input, init);
|
|
87
|
+
if (!res.ok) return res;
|
|
88
|
+
const buf = await res.arrayBuffer();
|
|
89
|
+
await putCached(url, buf);
|
|
90
|
+
if (onProgress) onProgress(1, 1, url.split('/').pop());
|
|
91
|
+
return new Response(buf, { status: 200, headers: { 'content-type': guessContentType(url) } });
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* @param {string} url
|
|
97
|
+
* @returns {string}
|
|
98
|
+
*/
|
|
99
|
+
function guessContentType(url) {
|
|
100
|
+
if (url.endsWith('.json')) return 'application/json';
|
|
101
|
+
if (url.endsWith('.onnx')) return 'application/octet-stream';
|
|
102
|
+
return 'application/octet-stream';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function hasAnyCachedModel() {
|
|
106
|
+
try {
|
|
107
|
+
const db = await openDb();
|
|
108
|
+
return new Promise((resolve) => {
|
|
109
|
+
const tx = db.transaction(STORE, 'readonly');
|
|
110
|
+
const req = tx.objectStore(STORE).count();
|
|
111
|
+
req.onsuccess = () => resolve(req.result > 0);
|
|
112
|
+
req.onerror = () => resolve(false);
|
|
113
|
+
});
|
|
114
|
+
} catch (_) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--bg: #0f1419;
|
|
3
|
+
--panel: #1a2332;
|
|
4
|
+
--text: #e7ecf3;
|
|
5
|
+
--muted: #8b9cb3;
|
|
6
|
+
--accent: #3d8bfd;
|
|
7
|
+
--active: #1e3a5f;
|
|
8
|
+
--border: #2a3548;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
* {
|
|
12
|
+
box-sizing: border-box;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
body {
|
|
16
|
+
margin: 0;
|
|
17
|
+
font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
|
|
18
|
+
background: var(--bg);
|
|
19
|
+
color: var(--text);
|
|
20
|
+
line-height: 1.5;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
header {
|
|
24
|
+
padding: 1rem 1.25rem;
|
|
25
|
+
border-bottom: 1px solid var(--border);
|
|
26
|
+
background: var(--panel);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
header h1 {
|
|
30
|
+
margin: 0 0 0.25rem;
|
|
31
|
+
font-size: 1.25rem;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
header .meta {
|
|
35
|
+
color: var(--muted);
|
|
36
|
+
font-size: 0.875rem;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
#status-bar {
|
|
40
|
+
padding: 0.75rem 1.25rem;
|
|
41
|
+
background: #15202b;
|
|
42
|
+
border-bottom: 1px solid var(--border);
|
|
43
|
+
font-size: 0.875rem;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
#status-bar.error {
|
|
47
|
+
color: #ff8a8a;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
#backend-badge {
|
|
51
|
+
display: inline-block;
|
|
52
|
+
margin-left: 0.5rem;
|
|
53
|
+
padding: 0.1rem 0.4rem;
|
|
54
|
+
border-radius: 4px;
|
|
55
|
+
background: #2d4a22;
|
|
56
|
+
font-size: 0.75rem;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.layout {
|
|
60
|
+
display: grid;
|
|
61
|
+
grid-template-columns: auto 1fr 320px;
|
|
62
|
+
gap: 0;
|
|
63
|
+
min-height: calc(100vh - 120px);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@media (max-width: 900px) {
|
|
67
|
+
.layout {
|
|
68
|
+
grid-template-columns: 1fr;
|
|
69
|
+
}
|
|
70
|
+
.file-sidebar { grid-column: auto; }
|
|
71
|
+
#sections { grid-column: auto; }
|
|
72
|
+
#controls { grid-column: auto; }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.file-sidebar {
|
|
76
|
+
grid-column: 1;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
#sections {
|
|
80
|
+
grid-column: 2;
|
|
81
|
+
padding: 1rem 1.25rem 2rem;
|
|
82
|
+
overflow-y: auto;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
#controls {
|
|
86
|
+
grid-column: 3;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.section {
|
|
90
|
+
margin-bottom: 1.25rem;
|
|
91
|
+
padding: 0.75rem 1rem;
|
|
92
|
+
border-radius: 8px;
|
|
93
|
+
border: 1px solid transparent;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.section.active {
|
|
97
|
+
background: var(--active);
|
|
98
|
+
border-color: var(--accent);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.section h2 {
|
|
102
|
+
margin: 0 0 0.5rem;
|
|
103
|
+
font-size: 1rem;
|
|
104
|
+
color: var(--accent);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/* Plain-text fallback: only apply pre-wrap when section-body holds raw text (no HTML tags) */
|
|
108
|
+
.section .section-body:not(:has(> p, > ul, > ol, > h1, > h2, > h3, > h4, > blockquote, > pre, > table)) {
|
|
109
|
+
white-space: pre-wrap;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/* Markdown-rendered HTML elements produced by marked */
|
|
113
|
+
.section h1,
|
|
114
|
+
.section h2,
|
|
115
|
+
.section h3,
|
|
116
|
+
.section h4 {
|
|
117
|
+
margin: 0.75rem 0 0.25rem;
|
|
118
|
+
font-size: 0.95rem;
|
|
119
|
+
color: var(--accent);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.section ul,
|
|
123
|
+
.section ol {
|
|
124
|
+
margin: 0.4rem 0 0.4rem 1.25rem;
|
|
125
|
+
padding: 0;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.section li {
|
|
129
|
+
margin-bottom: 0.2rem;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.section code {
|
|
133
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
134
|
+
font-size: 0.85em;
|
|
135
|
+
background: rgba(255, 255, 255, 0.08);
|
|
136
|
+
border-radius: 3px;
|
|
137
|
+
padding: 0.1em 0.3em;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.section pre {
|
|
141
|
+
background: rgba(255, 255, 255, 0.05);
|
|
142
|
+
border: 1px solid var(--border);
|
|
143
|
+
border-radius: 6px;
|
|
144
|
+
padding: 0.6rem 0.8rem;
|
|
145
|
+
overflow-x: auto;
|
|
146
|
+
font-size: 0.82rem;
|
|
147
|
+
margin: 0.5rem 0;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.section pre code {
|
|
151
|
+
background: transparent;
|
|
152
|
+
padding: 0;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.section blockquote {
|
|
156
|
+
border-left: 3px solid var(--accent);
|
|
157
|
+
margin: 0.5rem 0;
|
|
158
|
+
padding: 0.25rem 0.75rem;
|
|
159
|
+
color: var(--muted);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.section table {
|
|
163
|
+
border-collapse: collapse;
|
|
164
|
+
width: 100%;
|
|
165
|
+
font-size: 0.85rem;
|
|
166
|
+
margin: 0.5rem 0;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.section thead {
|
|
170
|
+
background: rgba(255, 255, 255, 0.06);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.section th,
|
|
174
|
+
.section td {
|
|
175
|
+
border: 1px solid var(--border);
|
|
176
|
+
padding: 0.3rem 0.5rem;
|
|
177
|
+
text-align: left;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.section tr:nth-child(even) td {
|
|
181
|
+
background: rgba(255, 255, 255, 0.03);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.section strong {
|
|
185
|
+
font-weight: 600;
|
|
186
|
+
color: var(--text);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.section em {
|
|
190
|
+
font-style: italic;
|
|
191
|
+
color: var(--muted);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.section p {
|
|
195
|
+
margin: 0.3rem 0;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
#controls {
|
|
199
|
+
padding: 1rem;
|
|
200
|
+
background: var(--panel);
|
|
201
|
+
border-left: 1px solid var(--border);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
#controls label {
|
|
205
|
+
display: block;
|
|
206
|
+
margin-bottom: 0.75rem;
|
|
207
|
+
font-size: 0.8rem;
|
|
208
|
+
color: var(--muted);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
#controls select,
|
|
212
|
+
#controls input[type="range"] {
|
|
213
|
+
width: 100%;
|
|
214
|
+
margin-top: 0.25rem;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
#lang-select:disabled {
|
|
218
|
+
opacity: 1;
|
|
219
|
+
cursor: default;
|
|
220
|
+
pointer-events: none;
|
|
221
|
+
background: #e8f0fe;
|
|
222
|
+
color: #1a5fc8;
|
|
223
|
+
border-color: #c5d3f0;
|
|
224
|
+
font-weight: 600;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.control-row {
|
|
228
|
+
display: flex;
|
|
229
|
+
gap: 0.5rem;
|
|
230
|
+
margin-bottom: 1rem;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.control-row button {
|
|
234
|
+
flex: 1;
|
|
235
|
+
padding: 0.5rem;
|
|
236
|
+
border: 1px solid var(--border);
|
|
237
|
+
border-radius: 6px;
|
|
238
|
+
background: #243044;
|
|
239
|
+
color: var(--text);
|
|
240
|
+
cursor: pointer;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.control-row button:hover {
|
|
244
|
+
background: #2f3f56;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.control-row button:disabled {
|
|
248
|
+
opacity: 0.5;
|
|
249
|
+
cursor: not-allowed;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
#play-btn {
|
|
253
|
+
background: var(--accent);
|
|
254
|
+
border-color: var(--accent);
|
|
255
|
+
color: #fff;
|
|
256
|
+
font-weight: 600;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.hidden {
|
|
260
|
+
display: none;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.file-sidebar {
|
|
264
|
+
padding: 0.75rem;
|
|
265
|
+
background: var(--panel);
|
|
266
|
+
border-right: 1px solid var(--border);
|
|
267
|
+
overflow-y: auto;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.sidebar-file-btn {
|
|
271
|
+
display: block;
|
|
272
|
+
width: 100%;
|
|
273
|
+
padding: 0.4rem 0.6rem;
|
|
274
|
+
margin-bottom: 0.25rem;
|
|
275
|
+
border: 1px solid transparent;
|
|
276
|
+
border-radius: 5px;
|
|
277
|
+
background: transparent;
|
|
278
|
+
color: var(--muted);
|
|
279
|
+
font-size: 0.82rem;
|
|
280
|
+
text-align: left;
|
|
281
|
+
cursor: pointer;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.sidebar-file-btn:hover {
|
|
285
|
+
background: #243044;
|
|
286
|
+
color: var(--text);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
.sidebar-file-btn.active {
|
|
290
|
+
background: var(--active);
|
|
291
|
+
border-color: var(--accent);
|
|
292
|
+
color: var(--text);
|
|
293
|
+
font-weight: 600;
|
|
294
|
+
}
|