feat(knowledge): SE practitioner-QA layer scaffold + live progress hook
Dump stackexchange_20260331 (newest mirror), 6 sites, downloads running; streamed ingest/filter/tags pipeline; status writer + homepage strip show the layer's build progress every 5 minutes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PDKeXvpT6tENSvyQGLV1Uq
This commit is contained in:
6
.gitignore
vendored
6
.gitignore
vendored
@@ -15,3 +15,9 @@ __pycache__/
|
||||
|
||||
# Local scratch
|
||||
tmp/
|
||||
|
||||
# SE knowledge layer bulk data
|
||||
knowledge/data/raw/
|
||||
knowledge/data/extracted/
|
||||
knowledge/data/se.db*
|
||||
|
||||
|
||||
@@ -207,6 +207,7 @@
|
||||
<span>market-evidence crawl started <b style="color:#25253d;" data-st="started">…</b></span>
|
||||
<span>running for <b style="color:#25253d;" data-st="elapsed">…</b></span>
|
||||
<span><b style="color:#25253d;" data-st="ollama">…</b> Ollama extraction requests</span>
|
||||
<span id="sf-se-status" style="display:none;">knowledge layer (Stack Exchange): <b style="color:#25253d;" data-st="se_step">…</b> <span style="color:#8a8a99;" data-st="se_detail"></span></span>
|
||||
<span style="color:#8a8a99;">status as of <span data-st="updated">…</span> · computed in <span data-st="ms">…</span> ms · auto-refreshes every 5 min</span>
|
||||
</div>
|
||||
<div style="margin-top:8px;font-size:13px;color:#8a8a99;line-height:1.5;">
|
||||
@@ -447,6 +448,12 @@
|
||||
setSt("started", s.started_at);
|
||||
setSt("elapsed", s.elapsed_h + " h " + s.elapsed_m + " min");
|
||||
setSt("ollama", Number(s.ollama_requests || 0).toLocaleString("en-US"));
|
||||
if (s.se_step) {
|
||||
var seEl = document.getElementById("sf-se-status");
|
||||
if (seEl) seEl.style.display = "";
|
||||
setSt("se_step", s.se_step);
|
||||
setSt("se_detail", s.se_detail ? s.se_detail.slice(0, 90) : "");
|
||||
}
|
||||
setSt("updated", s.updated_at);
|
||||
setSt("ms", s.compute_ms);
|
||||
}).catch(function () {});
|
||||
|
||||
56
knowledge/DECISIONS.md
Normal file
56
knowledge/DECISIONS.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# DECISIONS — Stack-Exchange-Wissensschicht
|
||||
|
||||
Autonome Entscheidungen dieser Pipeline, chronologisch.
|
||||
|
||||
## D1 — Dump-Version
|
||||
`stackexchange_20260331` (archive.org-Suche am 2026-07-09 ergab einen neueren
|
||||
Mirror als den im Briefing genannten `stackexchange_20251231`). Dateien liegen
|
||||
im Unterordner `stackexchange_20260331/`; alle 6 Ziel-Archive vorhanden,
|
||||
zusammen ~929 MB (Meta-Sites bewusst NICHT geladen).
|
||||
|
||||
## D2 — Projektort & Repo
|
||||
Als Unterprojekt `knowledge/` im bestehenden Pipeline-Repo `C:\dev\skillfactor`
|
||||
(Branch `quality/reference-standard`), nicht als separates Repo: die
|
||||
Homepage-Quellen (`gitea-custom/templates/home.tmpl`), stats.json-Erzeugung
|
||||
und die Provenance-Maschinerie (p3d) leben hier — die SE-Integration greift
|
||||
direkt in diese Bausteine.
|
||||
|
||||
## D3 — Referenz-Taxonomien
|
||||
KEIN Neu-Download: ESCO v1.2.1 und O*NET 30.3 liegen bereits vollständig als
|
||||
Tabellen im lokalen MSSQL (`skillfactor`-DB: esco_occupation, esco_skill,
|
||||
esco_occ_skill, onet_occupation, onet_task, onet_software, …), geladen von den
|
||||
offiziellen Quellen in einer früheren Pipeline-Phase. Wiederverwendung ist
|
||||
idempotent und spart Zeit/Bandbreite; Versionen sind in den Paket-Manifests
|
||||
dokumentiert.
|
||||
|
||||
## D4 — 7z-Entpackung
|
||||
`py7zr` (pip) statt System-7-Zip: keine Binary-Abhängigkeit, selektive
|
||||
Extraktion nur von `Posts.xml` + `Tags.xml` je Site.
|
||||
|
||||
## D5 — LLM-Arbeitsteilung (Kosten/Kontext)
|
||||
- Tag→Taxonomie-Mapping: Claude klassifiziert die frequenzstärksten Tags
|
||||
direkt in Batches (Sprachverständnis, Konfidenz + Rationale); der Long-Tail
|
||||
(niedrige Frequenz nahe der 20er-Schwelle) läuft über das lokale
|
||||
gemma3:27b mit Claude-designtem Prompt und wird stichprobengeprüft.
|
||||
Grund: mehrere tausend Tags × Rationale sprengen das Session-Kontextbudget.
|
||||
- Q&A-Zusammenfassungen (Schritt 4, ~10×150 Einträge): lokales gemma3
|
||||
(etabliertes Muster der Evidence-Pipeline: Ollama extrahiert, Claude
|
||||
designt Prompt + validiert + macht Stichproben-QA). Läuft detached, GPU
|
||||
wird mit dem laufenden Evidence-Crawl geteilt.
|
||||
|
||||
## D6 — Homepage-Integration
|
||||
Der Homepage-Code LIEGT im Arbeitsverzeichnis (Live-Template
|
||||
`Tools\gitea\custom\templates\home.tmpl`, Spiegel in
|
||||
`gitea-custom/templates/`). Integration erfolgt daher direkt (kein
|
||||
`homepage/`-Snippet-Ordner nötig): Stack Exchange wird 6. Provenance-Kategorie
|
||||
(`stackx`) in p3d/p4/stats.json; der Donut-/Legenden-Anteil bemisst sich
|
||||
datenbasiert aus den Content-Items der kompilierten Q&A-Einträge (gleiche
|
||||
Zählmethode wie alle anderen Quellen: count_items über die generierten
|
||||
Dateien). Für die 10 Eval-Berufe wird das kompilierte Wissen zusätzlich als
|
||||
`references/practitioner-qa.md` in die zugehörigen Skill-Pakete gelegt —
|
||||
damit ist der Pie-Anteil echt gemessen, nicht angenommen.
|
||||
|
||||
## D7 — Live-Fortschritt (Nutzerwunsch 2026-07-09)
|
||||
Die Pipeline schreibt `knowledge/data/progress-se.json` nach jedem Schritt;
|
||||
der bestehende 5-Minuten-Status-Writer merged das in `assets/status.json`,
|
||||
die Homepage-Statusleiste zeigt den SE-Layer-Stand neben dem Crawl-Zähler.
|
||||
48
knowledge/README.md
Normal file
48
knowledge/README.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# SkillFactor knowledge layer — Stack Exchange practitioner Q&A
|
||||
|
||||
Curated practitioner knowledge from the Stack Exchange data dumps
|
||||
(CC-BY-SA 4.0), mapped onto ESCO skills / O*NET occupations and compiled
|
||||
into per-profession knowledge files.
|
||||
|
||||
**Dump version:** `stackexchange_20260331` (archive.org community mirror —
|
||||
newest at build time, see DECISIONS.md D1). Sites: workplace, pm, law,
|
||||
money, softwareengineering, datascience (~929 MB compressed).
|
||||
|
||||
## Setup
|
||||
|
||||
```
|
||||
# venv of the parent project is reused
|
||||
..\.venv\Scripts\pip install py7zr
|
||||
powershell -File scripts\download.ps1 # resumable, skips existing
|
||||
```
|
||||
|
||||
## Pipeline (idempotent, in order)
|
||||
|
||||
```
|
||||
python pipeline\se_pipeline.py ingest # 7z -> SQLite (streamed iterparse)
|
||||
python pipeline\se_pipeline.py filter # quality gates -> qa_pairs
|
||||
python pipeline\se_pipeline.py tags # tag frequencies >= 20 -> CSV
|
||||
# -> LLM tag mapping produces mappings\tag_to_skill.csv (see DECISIONS D5)
|
||||
python pipeline\se_pipeline.py compile # professions\<slug>\knowledge.md + vocabulary.json
|
||||
python pipeline\se_pipeline.py verify # consistency checks (exit != 0 on failure)
|
||||
```
|
||||
|
||||
Every step writes `data/progress-se.json`; the homepage status strip picks
|
||||
it up within 5 minutes (see DECISIONS D7).
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
data/raw/ downloaded 7z archives (git-ignored)
|
||||
data/extracted/ Posts/Tags/Users.xml per site (git-ignored)
|
||||
data/se.db SQLite: questions/answers/users/qa_pairs (git-ignored)
|
||||
mappings/ tag_to_skill.csv, unmapped.csv (versioned)
|
||||
professions/ <slug>/knowledge.md, vocabulary.json (versioned)
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
Source content: Stack Exchange Network, **CC-BY-SA 4.0** — see
|
||||
`ATTRIBUTION.md`. Compiled knowledge files are derivative works and carry
|
||||
per-entry attribution (question link, author, license); published derived
|
||||
content must remain share-alike.
|
||||
277
knowledge/pipeline/se_pipeline.py
Normal file
277
knowledge/pipeline/se_pipeline.py
Normal file
@@ -0,0 +1,277 @@
|
||||
"""SkillFactor knowledge layer — Stack Exchange practitioner Q&A.
|
||||
|
||||
Reproducible pipeline over the archive.org data-dump mirror
|
||||
(stackexchange_20260331, CC-BY-SA 4.0). See knowledge/README.md.
|
||||
|
||||
CLI:
|
||||
python knowledge/pipeline/se_pipeline.py ingest 7z -> SQLite (streamed)
|
||||
python knowledge/pipeline/se_pipeline.py filter quality gates
|
||||
python knowledge/pipeline/se_pipeline.py tags tag frequency export
|
||||
python knowledge/pipeline/se_pipeline.py compile profession knowledge files
|
||||
python knowledge/pipeline/se_pipeline.py verify consistency checks
|
||||
|
||||
Idempotent: existing archives are not re-downloaded (scripts/download.ps1),
|
||||
extraction/ingest skip completed sites, summaries are cached per post id.
|
||||
Every step updates data/progress-se.json for the homepage live status.
|
||||
"""
|
||||
import argparse
|
||||
import html
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sqlite3
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from xml.etree import ElementTree as ET
|
||||
|
||||
K = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # knowledge/
|
||||
RAW = os.path.join(K, "data", "raw")
|
||||
EXTRACTED = os.path.join(K, "data", "extracted")
|
||||
DB = os.path.join(K, "data", "se.db")
|
||||
PROGRESS = os.path.join(K, "data", "progress-se.json")
|
||||
|
||||
DUMP_VERSION = "stackexchange_20260331"
|
||||
DUMP_URL = "https://archive.org/download/stackexchange_20260331/"
|
||||
|
||||
SITES = {
|
||||
"workplace": "workplace.stackexchange.com",
|
||||
"pm": "pm.stackexchange.com",
|
||||
"law": "law.stackexchange.com",
|
||||
"money": "money.stackexchange.com",
|
||||
"softwareengineering": "softwareengineering.stackexchange.com",
|
||||
"datascience": "datascience.stackexchange.com",
|
||||
}
|
||||
|
||||
MIN_Q_SCORE = 5
|
||||
MIN_A_SCORE = 5
|
||||
MIN_ANSWER_CHARS = 300
|
||||
TAG_MIN_FREQ = 20
|
||||
|
||||
|
||||
def progress(step, **info):
|
||||
data = {}
|
||||
if os.path.exists(PROGRESS):
|
||||
try:
|
||||
data = json.load(open(PROGRESS, encoding="utf-8"))
|
||||
except ValueError:
|
||||
data = {}
|
||||
data["step"] = step
|
||||
data["updated_at"] = datetime.now().isoformat(timespec="seconds")
|
||||
data.setdefault("steps", {})[step] = {
|
||||
"at": data["updated_at"], **info}
|
||||
json.dump(data, open(PROGRESS, "w", encoding="utf-8"), indent=1)
|
||||
|
||||
|
||||
def strip_html(s):
|
||||
if not s:
|
||||
return ""
|
||||
s = re.sub(r"<(pre|code)[^>]*>(.*?)</\1>",
|
||||
lambda m: "\n```\n" + m.group(2) + "\n```\n", s, flags=re.S)
|
||||
s = re.sub(r"<br\s*/?>|</p>|</li>", "\n", s)
|
||||
s = re.sub(r"<[^>]+>", "", s)
|
||||
return html.unescape(s).strip()
|
||||
|
||||
|
||||
def db():
|
||||
cn = sqlite3.connect(DB)
|
||||
cn.execute("PRAGMA journal_mode=WAL")
|
||||
return cn
|
||||
|
||||
|
||||
def ensure_schema(cn):
|
||||
cn.executescript("""
|
||||
CREATE TABLE IF NOT EXISTS questions(
|
||||
site TEXT, id INTEGER, title TEXT, body TEXT, tags TEXT,
|
||||
score INTEGER, accepted_answer_id INTEGER, view_count INTEGER,
|
||||
closed INTEGER, owner_id INTEGER, PRIMARY KEY(site, id));
|
||||
CREATE TABLE IF NOT EXISTS answers(
|
||||
site TEXT, id INTEGER, parent_id INTEGER, body TEXT,
|
||||
score INTEGER, owner_id INTEGER, PRIMARY KEY(site, id));
|
||||
CREATE TABLE IF NOT EXISTS users(
|
||||
site TEXT, id INTEGER, display_name TEXT, PRIMARY KEY(site, id));
|
||||
CREATE TABLE IF NOT EXISTS ingest_log(
|
||||
site TEXT PRIMARY KEY, questions INTEGER, answers INTEGER,
|
||||
users INTEGER, at TEXT);
|
||||
CREATE INDEX IF NOT EXISTS ix_answers_parent ON answers(site, parent_id);
|
||||
""")
|
||||
|
||||
|
||||
def cmd_ingest():
|
||||
import py7zr
|
||||
cn = db()
|
||||
ensure_schema(cn)
|
||||
done = {r[0] for r in cn.execute("SELECT site FROM ingest_log")}
|
||||
for key, host in SITES.items():
|
||||
if key in done:
|
||||
print(f"{key}: already ingested — skip")
|
||||
continue
|
||||
arc = os.path.join(RAW, f"{host}.7z")
|
||||
if not os.path.exists(arc):
|
||||
print(f"{key}: archive missing ({arc}) — skip")
|
||||
continue
|
||||
xdir = os.path.join(EXTRACTED, key)
|
||||
os.makedirs(xdir, exist_ok=True)
|
||||
if not os.path.exists(os.path.join(xdir, "Posts.xml")):
|
||||
print(f"{key}: extracting Posts/Tags/Users ...")
|
||||
with py7zr.SevenZipFile(arc) as z:
|
||||
names = [n for n in z.getnames()
|
||||
if n in ("Posts.xml", "Tags.xml", "Users.xml")]
|
||||
z.extract(path=xdir, targets=names)
|
||||
|
||||
nq = na = nu = 0
|
||||
qb, ab = [], []
|
||||
for _ev, el in ET.iterparse(os.path.join(xdir, "Posts.xml")):
|
||||
if el.tag != "row":
|
||||
continue
|
||||
pt = el.get("PostTypeId")
|
||||
if pt == "1":
|
||||
qb.append((key, int(el.get("Id")), el.get("Title", ""),
|
||||
strip_html(el.get("Body", "")),
|
||||
el.get("Tags", ""), int(el.get("Score", 0)),
|
||||
int(el.get("AcceptedAnswerId", 0) or 0),
|
||||
int(el.get("ViewCount", 0) or 0),
|
||||
1 if el.get("ClosedDate") else 0,
|
||||
int(el.get("OwnerUserId", 0) or 0)))
|
||||
nq += 1
|
||||
elif pt == "2":
|
||||
ab.append((key, int(el.get("Id")),
|
||||
int(el.get("ParentId", 0) or 0),
|
||||
strip_html(el.get("Body", "")),
|
||||
int(el.get("Score", 0)),
|
||||
int(el.get("OwnerUserId", 0) or 0)))
|
||||
na += 1
|
||||
el.clear()
|
||||
if len(qb) >= 2000:
|
||||
cn.executemany("INSERT OR REPLACE INTO questions VALUES(?,?,?,?,?,?,?,?,?,?)", qb)
|
||||
qb = []
|
||||
if len(ab) >= 2000:
|
||||
cn.executemany("INSERT OR REPLACE INTO answers VALUES(?,?,?,?,?,?)", ab)
|
||||
ab = []
|
||||
if qb:
|
||||
cn.executemany("INSERT OR REPLACE INTO questions VALUES(?,?,?,?,?,?,?,?,?,?)", qb)
|
||||
if ab:
|
||||
cn.executemany("INSERT OR REPLACE INTO answers VALUES(?,?,?,?,?,?)", ab)
|
||||
|
||||
upath = os.path.join(xdir, "Users.xml")
|
||||
if os.path.exists(upath):
|
||||
ub = []
|
||||
for _ev, el in ET.iterparse(upath):
|
||||
if el.tag == "row":
|
||||
ub.append((key, int(el.get("Id")),
|
||||
el.get("DisplayName", "")))
|
||||
nu += 1
|
||||
el.clear()
|
||||
if len(ub) >= 5000:
|
||||
cn.executemany("INSERT OR REPLACE INTO users VALUES(?,?,?)", ub)
|
||||
ub = []
|
||||
if ub:
|
||||
cn.executemany("INSERT OR REPLACE INTO users VALUES(?,?,?)", ub)
|
||||
|
||||
cn.execute("INSERT OR REPLACE INTO ingest_log VALUES(?,?,?,?,?)",
|
||||
(key, nq, na, nu, datetime.now().isoformat()))
|
||||
cn.commit()
|
||||
print(f"{key}: {nq} questions, {na} answers, {nu} users")
|
||||
progress("ingest", site=key, questions=nq, answers=na)
|
||||
cn.close()
|
||||
progress("ingest-done")
|
||||
|
||||
|
||||
def cmd_filter():
|
||||
cn = db()
|
||||
cn.executescript("""
|
||||
DROP TABLE IF EXISTS qa_pairs;
|
||||
CREATE TABLE qa_pairs(
|
||||
site TEXT, q_id INTEGER, title TEXT, q_body TEXT, tags TEXT,
|
||||
q_score INTEGER, view_count INTEGER,
|
||||
a_id INTEGER, a_body TEXT, a_score INTEGER, a_accepted INTEGER,
|
||||
q_owner INTEGER, a_owner INTEGER,
|
||||
PRIMARY KEY(site, q_id, a_id));
|
||||
""")
|
||||
stats = {}
|
||||
for key in SITES:
|
||||
before = cn.execute(
|
||||
"SELECT COUNT(*) FROM questions WHERE site=?", (key,)).fetchone()[0]
|
||||
# accepted + top-scored answer per qualifying question
|
||||
cn.execute("""
|
||||
INSERT OR REPLACE INTO qa_pairs
|
||||
SELECT q.site, q.id, q.title, q.body, q.tags, q.score, q.view_count,
|
||||
a.id, a.body, a.score,
|
||||
CASE WHEN a.id = q.accepted_answer_id THEN 1 ELSE 0 END,
|
||||
q.owner_id, a.owner_id
|
||||
FROM questions q
|
||||
JOIN answers a ON a.site = q.site AND a.parent_id = q.id
|
||||
WHERE q.site = ?
|
||||
AND q.score >= ?
|
||||
AND q.closed = 0
|
||||
AND LENGTH(a.body) >= ?
|
||||
AND (a.id = q.accepted_answer_id OR
|
||||
a.id = (SELECT a2.id FROM answers a2
|
||||
WHERE a2.site = q.site AND a2.parent_id = q.id
|
||||
ORDER BY a2.score DESC, a2.id LIMIT 1))
|
||||
AND EXISTS (SELECT 1 FROM answers ax
|
||||
WHERE ax.site = q.site AND ax.parent_id = q.id
|
||||
AND (ax.score >= ? OR ax.id = q.accepted_answer_id))
|
||||
""", (key, MIN_Q_SCORE, MIN_ANSWER_CHARS, MIN_A_SCORE))
|
||||
after_q = cn.execute(
|
||||
"SELECT COUNT(DISTINCT q_id) FROM qa_pairs WHERE site=?",
|
||||
(key,)).fetchone()[0]
|
||||
after_p = cn.execute(
|
||||
"SELECT COUNT(*) FROM qa_pairs WHERE site=?", (key,)).fetchone()[0]
|
||||
stats[key] = {"questions_raw": before, "questions_kept": after_q,
|
||||
"pairs_kept": after_p}
|
||||
print(f"{key}: {before} raw questions -> {after_q} kept "
|
||||
f"({after_p} Q&A pairs)")
|
||||
cn.commit()
|
||||
cn.close()
|
||||
json.dump(stats, open(os.path.join(K, "data", "filter-stats.json"), "w",
|
||||
encoding="utf-8"), indent=1)
|
||||
progress("filter-done", **{k: v["questions_kept"] for k, v in stats.items()})
|
||||
|
||||
|
||||
def cmd_tags():
|
||||
cn = db()
|
||||
out = os.path.join(K, "data", "tags_freq.csv")
|
||||
rows = []
|
||||
for key in SITES:
|
||||
freq = {}
|
||||
for (tags,) in cn.execute(
|
||||
"SELECT tags FROM qa_pairs WHERE site=?", (key,)):
|
||||
for t in re.findall(r"[<|]([^><|]+)[>|]", tags or ""):
|
||||
freq[t] = freq.get(t, 0) + 1
|
||||
for t, n in sorted(freq.items(), key=lambda kv: -kv[1]):
|
||||
if n >= TAG_MIN_FREQ:
|
||||
rows.append((key, t, n))
|
||||
with open(out, "w", encoding="utf-8", newline="\n") as f:
|
||||
f.write("site,tag,count\n")
|
||||
for r in rows:
|
||||
f.write(f"{r[0]},{r[1]},{r[2]}\n")
|
||||
print(f"{len(rows)} tags with freq >= {TAG_MIN_FREQ} -> {out}")
|
||||
per_site = {}
|
||||
for s, _t, _n in rows:
|
||||
per_site[s] = per_site.get(s, 0) + 1
|
||||
print(per_site)
|
||||
progress("tags-done", tags_total=len(rows), **per_site)
|
||||
cn.close()
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("cmd", choices=["ingest", "filter", "tags",
|
||||
"compile", "verify"])
|
||||
args = ap.parse_args()
|
||||
if args.cmd == "ingest":
|
||||
cmd_ingest()
|
||||
elif args.cmd == "filter":
|
||||
cmd_filter()
|
||||
elif args.cmd == "tags":
|
||||
cmd_tags()
|
||||
elif args.cmd == "compile":
|
||||
from se_compile import cmd_compile
|
||||
cmd_compile()
|
||||
elif args.cmd == "verify":
|
||||
from se_verify import cmd_verify
|
||||
cmd_verify()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
22
knowledge/scripts/download.ps1
Normal file
22
knowledge/scripts/download.ps1
Normal file
@@ -0,0 +1,22 @@
|
||||
# Lädt die 6 Stack-Exchange-Site-Archive vom archive.org-Mirror (resumable).
|
||||
# Version: stackexchange_20260331 (neuester Mirror, geprüft 2026-07-09).
|
||||
$base = "https://archive.org/download/stackexchange_20260331/stackexchange_20260331"
|
||||
$dest = "C:\dev\skillfactor\knowledge\data\raw"
|
||||
New-Item -ItemType Directory -Force $dest | Out-Null
|
||||
$files = @(
|
||||
"workplace.stackexchange.com.7z",
|
||||
"pm.stackexchange.com.7z",
|
||||
"law.stackexchange.com.7z",
|
||||
"money.stackexchange.com.7z",
|
||||
"softwareengineering.stackexchange.com.7z",
|
||||
"datascience.stackexchange.com.7z"
|
||||
)
|
||||
$log = "C:\dev\skillfactor\docs\logs\se_download.log"
|
||||
foreach ($f in $files) {
|
||||
"$(Get-Date -Format 'HH:mm:ss') start $f" | Out-File $log -Append -Encoding utf8
|
||||
# curl.exe: resumable (-C -), folgt Redirects (-L), still (-sS)
|
||||
& curl.exe -L -sS -C - -o "$dest\$f" "$base/$f" 2>&1 | Out-File $log -Append -Encoding utf8
|
||||
$size = [Math]::Round((Get-Item "$dest\$f").Length / 1MB)
|
||||
"$(Get-Date -Format 'HH:mm:ss') done $f ($size MB)" | Out-File $log -Append -Encoding utf8
|
||||
}
|
||||
"$(Get-Date -Format 'HH:mm:ss') ALL DOWNLOADS COMPLETE" | Out-File $log -Append -Encoding utf8
|
||||
@@ -54,6 +54,12 @@ while ($true) {
|
||||
$startDt = [datetime]::ParseExact($started, "yyyy-MM-dd HH:mm", $null)
|
||||
$elapsed = (Get-Date) - $startDt
|
||||
$sw.Stop()
|
||||
# Stack-Exchange-Wissensschicht: Fortschritt der knowledge-Pipeline
|
||||
$se = $null
|
||||
$seFile = "$dev\knowledge\data\progress-se.json"
|
||||
if (Test-Path $seFile) {
|
||||
try { $se = Get-Content $seFile -Raw -Encoding utf8 | ConvertFrom-Json } catch {}
|
||||
}
|
||||
$obj = [ordered]@{
|
||||
done = $done
|
||||
total = $total
|
||||
@@ -62,6 +68,11 @@ while ($true) {
|
||||
elapsed_h = [Math]::Floor($elapsed.TotalHours)
|
||||
elapsed_m = $elapsed.Minutes
|
||||
ollama_requests = $ollama
|
||||
se_step = $(if ($se) { $se.step } else { $null })
|
||||
se_updated = $(if ($se) { $se.updated_at } else { $null })
|
||||
se_detail = $(if ($se -and $se.steps -and $se.step -and $se.steps.($se.step)) {
|
||||
($se.steps.($se.step) | ConvertTo-Json -Compress)
|
||||
} else { $null })
|
||||
updated_at = (Get-Date -Format "yyyy-MM-dd HH:mm:ss")
|
||||
compute_ms = $sw.ElapsedMilliseconds
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user