Spaces:
Running
Running
DataInventor: open-source Invent a Dataset recreation
Browse files- Dockerfile +12 -2
- README.md +30 -5
- app/engine.py +328 -0
- app/main.py +168 -0
- requirements.txt +3 -0
- static/app.css +190 -0
- static/app.js +255 -0
- static/domains.json +716 -0
- static/index.html +35 -0
Dockerfile
CHANGED
|
@@ -1,2 +1,12 @@
|
|
| 1 |
-
FROM
|
| 2 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.12-slim
|
| 2 |
+
ENV PYTHONUNBUFFERED=1 PORT=7860
|
| 3 |
+
RUN useradd -m -u 1000 user
|
| 4 |
+
USER user
|
| 5 |
+
ENV PATH="/home/user/.local/bin:$PATH"
|
| 6 |
+
WORKDIR /home/user/app
|
| 7 |
+
COPY --chown=user requirements.txt .
|
| 8 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 9 |
+
COPY --chown=user app ./app
|
| 10 |
+
COPY --chown=user static ./static
|
| 11 |
+
WORKDIR /home/user/app/app
|
| 12 |
+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
|
README.md
CHANGED
|
@@ -1,9 +1,34 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
|
|
|
| 7 |
pinned: false
|
| 8 |
-
|
| 9 |
---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: DataInventor
|
| 3 |
+
emoji: 🧪
|
| 4 |
+
colorFrom: indigo
|
| 5 |
+
colorTo: pink
|
| 6 |
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
pinned: false
|
| 9 |
+
short_description: Describe a dataset in plain words, get training rows back.
|
| 10 |
---
|
| 11 |
+
|
| 12 |
+
# DataInventor
|
| 13 |
+
|
| 14 |
+
An open-source recreation of the [Invent a Dataset](https://adaptionlabs.ai/invent-a-dataset) workflow. Describe the data you want, choose a format, domains and languages, preview a blueprint, and get an instruction or preference dataset back as CSV or JSONL.
|
| 15 |
+
|
| 16 |
+
It works with any OpenAI-compatible endpoint. The default is a free model served through Hugging Face Inference Providers.
|
| 17 |
+
|
| 18 |
+
## How it writes rows
|
| 19 |
+
|
| 20 |
+
1. **Blueprint.** The model turns your description into topics, personas, who answers and the shapes questions take.
|
| 21 |
+
2. **Sampling.** Unique combinations of topic, persona, shape, difficulty, length and language.
|
| 22 |
+
3. **Writing.** One message per combination, then the reply (preference pairs get two replies and a ranking).
|
| 23 |
+
4. **Filters.** Cut-off or degenerate text, requests for many items at once, and near-duplicate questions are rejected.
|
| 24 |
+
|
| 25 |
+
## Run it yourself
|
| 26 |
+
|
| 27 |
+
```bash
|
| 28 |
+
pip install -r requirements.txt
|
| 29 |
+
cd app && HF_TOKEN=hf_... uvicorn main:app --port 7860
|
| 30 |
+
```
|
| 31 |
+
|
| 32 |
+
Settings: `MAX_ROWS` (default 1000), `MAX_RUNNING` (default 2), `HF_TOKEN` (used only for the Hugging Face router).
|
| 33 |
+
|
| 34 |
+
Not affiliated with Adaption Labs.
|
app/engine.py
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""DataInventor engine: one sentence in, a training dataset out, on any OpenAI-compatible endpoint.
|
| 2 |
+
|
| 3 |
+
Pipeline: blueprint (taxonomy + personas + question shapes) -> sampled combinations ->
|
| 4 |
+
question -> answer (or two answers + a ranking for preference pairs) -> filters -> near-duplicate rejection.
|
| 5 |
+
"""
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
|
| 8 |
+
import json
|
| 9 |
+
import random
|
| 10 |
+
import re
|
| 11 |
+
import threading
|
| 12 |
+
import time
|
| 13 |
+
import urllib.request
|
| 14 |
+
import urllib.error
|
| 15 |
+
from concurrent.futures import ThreadPoolExecutor
|
| 16 |
+
from dataclasses import dataclass, field
|
| 17 |
+
from typing import Callable, Optional
|
| 18 |
+
|
| 19 |
+
HF_BASE = "https://router.huggingface.co/v1"
|
| 20 |
+
HF_MODEL = "prism-ml/Ternary-Bonsai-27B-AWQ-4bit:together"
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
@dataclass
|
| 24 |
+
class Endpoint:
|
| 25 |
+
base_url: str = HF_BASE
|
| 26 |
+
api_key: str = ""
|
| 27 |
+
model: str = HF_MODEL
|
| 28 |
+
disable_thinking: bool = True
|
| 29 |
+
workers: int = 4
|
| 30 |
+
|
| 31 |
+
def chat(self, prompt: str, max_tokens: int = 1500, temperature: float = 0.8, retries: int = 5,
|
| 32 |
+
validate: Optional[Callable[[str], bool]] = None) -> str:
|
| 33 |
+
body = {"model": self.model, "max_tokens": max_tokens, "temperature": temperature,
|
| 34 |
+
"messages": [{"role": "user", "content": prompt}]}
|
| 35 |
+
if self.disable_thinking:
|
| 36 |
+
body["chat_template_kwargs"] = {"enable_thinking": False}
|
| 37 |
+
data = json.dumps(body).encode()
|
| 38 |
+
url = self.base_url.rstrip("/") + "/chat/completions"
|
| 39 |
+
last = "no attempt"
|
| 40 |
+
for attempt in range(retries):
|
| 41 |
+
try:
|
| 42 |
+
req = urllib.request.Request(url, data, {"Content-Type": "application/json", "User-Agent": "DataInventor/1.0",
|
| 43 |
+
"Authorization": "Bearer " + self.api_key})
|
| 44 |
+
resp = json.load(urllib.request.urlopen(req, timeout=180))
|
| 45 |
+
choice = (resp.get("choices") or [{}])[0] or {}
|
| 46 |
+
if choice.get("finish_reason") == "length":
|
| 47 |
+
last = "reply cut off at max_tokens"
|
| 48 |
+
time.sleep(1.5)
|
| 49 |
+
continue
|
| 50 |
+
text = ((choice.get("message") or {}).get("content") or "").strip()
|
| 51 |
+
text = re.sub(r"<think>.*?</think>", "", text, flags=re.S).strip()
|
| 52 |
+
if text and (validate is None or validate(text)):
|
| 53 |
+
return text
|
| 54 |
+
last = "reply rejected by a quality check"
|
| 55 |
+
except urllib.error.HTTPError as e:
|
| 56 |
+
detail = ""
|
| 57 |
+
try:
|
| 58 |
+
detail = e.read().decode()[:200]
|
| 59 |
+
except Exception:
|
| 60 |
+
pass
|
| 61 |
+
last = f"HTTP {e.code} {detail}"
|
| 62 |
+
if e.code in (401, 403):
|
| 63 |
+
raise EndpointError(f"The endpoint refused the key (HTTP {e.code}). Check the API key.")
|
| 64 |
+
if e.code == 404:
|
| 65 |
+
raise EndpointError("The endpoint or model was not found (HTTP 404). Check the base URL and model name.")
|
| 66 |
+
time.sleep(min(30, 2 ** attempt * 2) if e.code == 429 else 3 * (attempt + 1))
|
| 67 |
+
except Exception as e: # timeouts, resets
|
| 68 |
+
last = str(e)[:160]
|
| 69 |
+
time.sleep(3 * (attempt + 1))
|
| 70 |
+
raise EndpointError(last)
|
| 71 |
+
|
| 72 |
+
def ping(self) -> dict:
|
| 73 |
+
t = time.time()
|
| 74 |
+
text = self.chat("Reply with the single word ready.", max_tokens=16, temperature=0, retries=2)
|
| 75 |
+
return {"ok": True, "latency_ms": int((time.time() - t) * 1000), "sample": text[:40]}
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
class EndpointError(Exception):
|
| 79 |
+
pass
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
# ---------------------------------------------------------------- blueprint
|
| 83 |
+
|
| 84 |
+
def _s(x) -> str:
|
| 85 |
+
"""Models sometimes return {"persona": "..."} instead of a bare string."""
|
| 86 |
+
if isinstance(x, dict):
|
| 87 |
+
for k in ("name", "persona", "role", "format", "shape", "topic", "description", "title"):
|
| 88 |
+
if isinstance(x.get(k), str):
|
| 89 |
+
return x[k].strip()
|
| 90 |
+
return " ".join(str(v) for v in x.values() if isinstance(v, str)).strip()
|
| 91 |
+
return str(x).strip()
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def normalize_blueprint(bp: dict) -> dict:
|
| 95 |
+
out = {"topics": [], "personas": [_s(p) for p in bp.get("personas", []) if _s(p)],
|
| 96 |
+
"formats": [_s(f) for f in bp.get("formats", []) if _s(f)],
|
| 97 |
+
"difficulty": [_s(d) for d in bp.get("difficulty", []) if _s(d)] or ["basic", "intermediate", "advanced"],
|
| 98 |
+
"assistant_role": _s(bp.get("assistant_role") or "an excellent, accurate AI assistant")[:300]}
|
| 99 |
+
for a in bp.get("topics", []):
|
| 100 |
+
subs = []
|
| 101 |
+
for sub in a.get("subtopics", []):
|
| 102 |
+
leaves = [_s(l) for l in sub.get("leaf_topics", []) if _s(l)]
|
| 103 |
+
if leaves:
|
| 104 |
+
subs.append({"subtopic": _s(sub.get("subtopic", "")), "leaf_topics": leaves})
|
| 105 |
+
if subs:
|
| 106 |
+
out["topics"].append({"area": _s(a.get("area", "")), "subtopics": subs})
|
| 107 |
+
if len(out["topics"]) < 1 or len(out["personas"]) < 2 or len(out["formats"]) < 2:
|
| 108 |
+
raise ValueError("the blueprint is missing topics, personas or formats")
|
| 109 |
+
return out
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def _json_from(text: str):
|
| 113 |
+
m = re.search(r"\{.*\}", text, re.S)
|
| 114 |
+
if not m:
|
| 115 |
+
raise ValueError("no JSON object in the reply")
|
| 116 |
+
return json.loads(m.group(0))
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def make_blueprint(ep: Endpoint, request: str, domains: list[str], subdomains: list[str], languages: list[str],
|
| 120 |
+
constraints: str, rows: int) -> dict:
|
| 121 |
+
scope = ""
|
| 122 |
+
if domains:
|
| 123 |
+
scope += f"Domains: {', '.join(domains)}.\n"
|
| 124 |
+
if subdomains:
|
| 125 |
+
scope += f"Subdomains: {', '.join(subdomains)}.\n"
|
| 126 |
+
if constraints.strip():
|
| 127 |
+
scope += f"Output requirements: {constraints.strip()}\n"
|
| 128 |
+
areas = 6 if rows <= 500 else 8
|
| 129 |
+
prompt = f"""You are planning a training dataset. Dataset request: "{request}"
|
| 130 |
+
{scope}
|
| 131 |
+
Return ONLY JSON with these keys:
|
| 132 |
+
"topics": a list of {areas} objects {{"area": str, "subtopics": [4 objects {{"subtopic": str, "leaf_topics": [4 specific, concrete strings]}}]}}. Cover the whole request evenly.
|
| 133 |
+
"personas": 10 different kinds of people who would write the user side of this dataset (for a customer-support dataset, 10 different kinds of customer; for a coding dataset, 10 kinds of developer).
|
| 134 |
+
"assistant_role": one sentence describing who answers in this dataset and in what voice (for example "a friendly support agent at a small online bookshop who gives concrete next steps").
|
| 135 |
+
"formats": 6 different shapes the instruction could take (for example a short direct ask, a detailed brief, a request with a concrete example, a request with constraints, a follow-up, a comparison).
|
| 136 |
+
"difficulty": ["basic","intermediate","advanced"].
|
| 137 |
+
No commentary, no code fences."""
|
| 138 |
+
last = None
|
| 139 |
+
for _ in range(4):
|
| 140 |
+
try:
|
| 141 |
+
return normalize_blueprint(_json_from(ep.chat(prompt, max_tokens=5000, temperature=0.7)))
|
| 142 |
+
except EndpointError:
|
| 143 |
+
raise
|
| 144 |
+
except Exception as e:
|
| 145 |
+
last = e
|
| 146 |
+
raise EndpointError(f"The model did not return a usable blueprint: {last}")
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
# ---------------------------------------------------------------- generation
|
| 150 |
+
|
| 151 |
+
LEAK = re.compile(r"the user (asks|wants)|write one|return only|^okay|^sure,|^here is|^here's|^as an ai", re.I)
|
| 152 |
+
END = re.compile(r"[.?!)\"”’'।:。!?]$")
|
| 153 |
+
MULTI = re.compile(r"\b(generate|draft|write|compile|create|give me|produce)\s+(\d+|several|multiple|a set of|a list of)\b", re.I)
|
| 154 |
+
LENGTHS = ["one blunt sentence", "two sentences with one specific detail",
|
| 155 |
+
"three or four sentences with concrete specifics", "a short paragraph that sounds a little rushed"]
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
def degenerate(t: str) -> bool:
|
| 159 |
+
w = t.lower().split()
|
| 160 |
+
return bool(re.search(r"(.)\1{7,}", t)) or (len(w) >= 8 and len(set(w)) / len(w) < 0.5)
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
def grams(t: str, n: int = 3) -> set:
|
| 164 |
+
w = t.lower().split()
|
| 165 |
+
return set(zip(*[w[i:] for i in range(n)])) if len(w) >= n else {tuple(w)}
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
@dataclass
|
| 169 |
+
class Job:
|
| 170 |
+
id: str
|
| 171 |
+
config: dict
|
| 172 |
+
state: str = "queued" # queued | planning | generating | done | failed | stopped
|
| 173 |
+
message: str = ""
|
| 174 |
+
blueprint: Optional[dict] = None
|
| 175 |
+
rows: list = field(default_factory=list)
|
| 176 |
+
target: int = 0
|
| 177 |
+
attempts: int = 0
|
| 178 |
+
rejected: dict = field(default_factory=dict)
|
| 179 |
+
started: float = field(default_factory=time.time)
|
| 180 |
+
finished: float = 0.0
|
| 181 |
+
stop: threading.Event = field(default_factory=threading.Event)
|
| 182 |
+
lock: threading.Lock = field(default_factory=threading.Lock)
|
| 183 |
+
|
| 184 |
+
def reject(self, why: str):
|
| 185 |
+
with self.lock:
|
| 186 |
+
self.rejected[why] = self.rejected.get(why, 0) + 1
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
def run_job(job: Job, ep: Endpoint):
|
| 190 |
+
cfg = job.config
|
| 191 |
+
rows_n = int(cfg["rows"])
|
| 192 |
+
job.target = rows_n
|
| 193 |
+
kind = cfg.get("format", "instruction")
|
| 194 |
+
request = cfg["request"]
|
| 195 |
+
languages = cfg.get("languages") or ["English"]
|
| 196 |
+
constraints = cfg.get("constraints", "")
|
| 197 |
+
try:
|
| 198 |
+
job.state, job.message = "planning", "Drafting the blueprint"
|
| 199 |
+
bp = normalize_blueprint(cfg["blueprint"]) if cfg.get("blueprint") else make_blueprint(
|
| 200 |
+
ep, request, cfg.get("domains", []), cfg.get("subdomains", []), languages, constraints, rows_n)
|
| 201 |
+
job.blueprint = bp
|
| 202 |
+
leaves = [(a["area"], s["subtopic"], l) for a in bp["topics"] for s in a["subtopics"] for l in s["leaf_topics"]]
|
| 203 |
+
rng = random.Random(cfg.get("seed", 7))
|
| 204 |
+
combos, seen = [], set()
|
| 205 |
+
want = int(rows_n * 1.8) + 20
|
| 206 |
+
guard = 0
|
| 207 |
+
while len(combos) < want and guard < want * 30:
|
| 208 |
+
guard += 1
|
| 209 |
+
c = (rng.choice(leaves), rng.choice(bp["personas"]), rng.choice(bp["formats"]), rng.choice(bp["difficulty"]),
|
| 210 |
+
rng.choice(LENGTHS), rng.choice(languages))
|
| 211 |
+
if c not in seen:
|
| 212 |
+
seen.add(c)
|
| 213 |
+
combos.append(c)
|
| 214 |
+
|
| 215 |
+
scope = f'The dataset being built: "{request}"'
|
| 216 |
+
if constraints.strip():
|
| 217 |
+
scope += f"\nEvery example must satisfy: {constraints.strip()}"
|
| 218 |
+
|
| 219 |
+
def ok_q(t):
|
| 220 |
+
t = t.strip().strip('"')
|
| 221 |
+
return 15 <= len(t) <= 1500 and END.search(t) and not degenerate(t) and not LEAK.search(t[:150]) and not MULTI.search(t[:120])
|
| 222 |
+
|
| 223 |
+
def ok_a(t):
|
| 224 |
+
return len(t.split()) >= 12 and not degenerate(t[:600]) and not LEAK.search(t[:100])
|
| 225 |
+
|
| 226 |
+
kept_grams: list[set] = []
|
| 227 |
+
job.state, job.message = "generating", "Writing examples"
|
| 228 |
+
|
| 229 |
+
def work(idx_combo):
|
| 230 |
+
idx, ((area, sub, leaf), persona, fmt, diff, length, lang) = idx_combo
|
| 231 |
+
if job.stop.is_set():
|
| 232 |
+
return
|
| 233 |
+
with job.lock:
|
| 234 |
+
if len(job.rows) >= rows_n:
|
| 235 |
+
return
|
| 236 |
+
job.attempts += 1
|
| 237 |
+
try:
|
| 238 |
+
role = bp.get("assistant_role", "an excellent AI assistant")
|
| 239 |
+
q = ep.chat(f"""{scope}
|
| 240 |
+
Write ONE realistic message that {persona} would send to {role}, for this dataset, about: {leaf} ({area}, {sub}).
|
| 241 |
+
Shape: {fmt}. Difficulty: {diff}. Length: {length}. Language: {lang}.
|
| 242 |
+
It should sound like a real person typing, specific and a little imperfect, not a textbook prompt. It must be ONE question or ONE task whose answer fits in a single reply: never ask for a list of many items, a set of Q&A pairs, or a dataset. Do not name the shape or the persona.
|
| 243 |
+
Return only the message.""", max_tokens=700, temperature=1.0, validate=ok_q).strip().strip('"')
|
| 244 |
+
g = grams(q)
|
| 245 |
+
with job.lock:
|
| 246 |
+
if any(len(g & k) / max(1, len(g | k)) > 0.5 for k in kept_grams):
|
| 247 |
+
job.reject("near-duplicate question")
|
| 248 |
+
return
|
| 249 |
+
answer_prompt = f"""You are {role}. Reply to the message below. The reply is a training example for this dataset: "{request}".
|
| 250 |
+
Language: {lang}. Be accurate, specific and complete. Start directly with the content, with no preamble about who you are. Do not invent statistics or facts you cannot know.
|
| 251 |
+
{('Requirements: ' + constraints.strip()) if constraints.strip() else ''}
|
| 252 |
+
|
| 253 |
+
Message: {q}"""
|
| 254 |
+
a1 = ep.chat(answer_prompt, max_tokens=2500, temperature=0.7, validate=ok_a)
|
| 255 |
+
row = {"area": area, "subtopic": sub, "topic": leaf, "persona": persona, "shape": fmt,
|
| 256 |
+
"difficulty": diff, "language": lang, "prompt": q}
|
| 257 |
+
if kind == "preference":
|
| 258 |
+
a2 = ep.chat(answer_prompt + "\n\nWrite a different answer from the obvious one, in a different structure and with different emphasis.",
|
| 259 |
+
max_tokens=2500, temperature=1.0, validate=ok_a)
|
| 260 |
+
verdict = ep.chat(f"""Which answer is better for the message? Judge accuracy, completeness and clarity.
|
| 261 |
+
Message: {q}
|
| 262 |
+
|
| 263 |
+
Answer A:
|
| 264 |
+
{a1[:3000]}
|
| 265 |
+
|
| 266 |
+
Answer B:
|
| 267 |
+
{a2[:3000]}
|
| 268 |
+
|
| 269 |
+
Reply with only the single letter A or B.""", max_tokens=8, temperature=0, validate=lambda s: s.strip()[:1].upper() in ("A", "B"))
|
| 270 |
+
a_wins = verdict.strip()[:1].upper() == "A"
|
| 271 |
+
row["chosen"], row["rejected"] = (a1, a2) if a_wins else (a2, a1)
|
| 272 |
+
else:
|
| 273 |
+
row["completion"] = a1
|
| 274 |
+
with job.lock:
|
| 275 |
+
if len(job.rows) >= rows_n:
|
| 276 |
+
return
|
| 277 |
+
if any(len(g & k) / max(1, len(g | k)) > 0.5 for k in kept_grams):
|
| 278 |
+
job.reject("near-duplicate question")
|
| 279 |
+
return
|
| 280 |
+
row["id"] = len(job.rows) + 1
|
| 281 |
+
job.rows.append(row)
|
| 282 |
+
kept_grams.append(g)
|
| 283 |
+
job.message = f"{len(job.rows)} of {rows_n} written"
|
| 284 |
+
if len(job.rows) >= rows_n:
|
| 285 |
+
job.state, job.message, job.finished = "done", f"{len(job.rows)} rows ready", time.time()
|
| 286 |
+
job.stop.set()
|
| 287 |
+
except EndpointError as e:
|
| 288 |
+
if "refused the key" in str(e) or "not found" in str(e):
|
| 289 |
+
job.state, job.message = "failed", str(e)
|
| 290 |
+
job.stop.set()
|
| 291 |
+
else:
|
| 292 |
+
job.reject("model call failed")
|
| 293 |
+
except Exception:
|
| 294 |
+
job.reject("model call failed")
|
| 295 |
+
|
| 296 |
+
with ThreadPoolExecutor(max_workers=max(1, min(ep.workers, 16))) as pool:
|
| 297 |
+
list(pool.map(work, enumerate(combos)))
|
| 298 |
+
if job.state in ("failed", "done"):
|
| 299 |
+
return
|
| 300 |
+
if job.stop.is_set():
|
| 301 |
+
job.state, job.message = "stopped", f"Stopped with {len(job.rows)} rows"
|
| 302 |
+
elif len(job.rows) < rows_n:
|
| 303 |
+
job.state = "done"
|
| 304 |
+
job.message = f"Finished with {len(job.rows)} of {rows_n} rows. The model rejected too many attempts to fill the rest."
|
| 305 |
+
else:
|
| 306 |
+
job.state, job.message = "done", f"{len(job.rows)} rows ready"
|
| 307 |
+
except EndpointError as e:
|
| 308 |
+
job.state, job.message = "failed", str(e)
|
| 309 |
+
except Exception as e: # never leave a job hanging
|
| 310 |
+
job.state, job.message = "failed", f"Unexpected error: {e}"
|
| 311 |
+
finally:
|
| 312 |
+
job.finished = time.time()
|
| 313 |
+
|
| 314 |
+
|
| 315 |
+
# ---------------------------------------------------------------- stats
|
| 316 |
+
|
| 317 |
+
def stats(rows: list) -> dict:
|
| 318 |
+
if not rows:
|
| 319 |
+
return {}
|
| 320 |
+
text = "\n".join(r["prompt"] for r in rows).lower().split()
|
| 321 |
+
tri = list(zip(text, text[1:], text[2:]))
|
| 322 |
+
area_counts: dict = {}
|
| 323 |
+
for r in rows:
|
| 324 |
+
area_counts[r["area"]] = area_counts.get(r["area"], 0) + 1
|
| 325 |
+
comp = [len((r.get("completion") or r.get("chosen") or "").split()) for r in rows]
|
| 326 |
+
return {"rows": len(rows), "distinct3_prompts": round(len(set(tri)) / max(1, len(tri)), 3),
|
| 327 |
+
"avg_prompt_words": round(sum(len(r["prompt"].split()) for r in rows) / len(rows), 1),
|
| 328 |
+
"avg_answer_words": round(sum(comp) / len(rows), 1), "areas": area_counts}
|
app/main.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""DataInventor: an open-source recreation of the Invent a Dataset workflow. Bring any OpenAI-compatible endpoint."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import csv
|
| 5 |
+
import io
|
| 6 |
+
import json
|
| 7 |
+
import os
|
| 8 |
+
import threading
|
| 9 |
+
import time
|
| 10 |
+
import uuid
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
|
| 13 |
+
from fastapi import FastAPI, HTTPException
|
| 14 |
+
from fastapi.responses import FileResponse, JSONResponse, Response
|
| 15 |
+
from fastapi.staticfiles import StaticFiles
|
| 16 |
+
from pydantic import BaseModel, Field
|
| 17 |
+
|
| 18 |
+
from engine import HF_BASE, HF_MODEL, Endpoint, EndpointError, Job, make_blueprint, run_job, stats
|
| 19 |
+
|
| 20 |
+
ROOT = Path(__file__).resolve().parent.parent
|
| 21 |
+
STATIC = ROOT / "static"
|
| 22 |
+
MAX_ROWS = int(os.environ.get("MAX_ROWS", "1000"))
|
| 23 |
+
MAX_RUNNING = int(os.environ.get("MAX_RUNNING", "2"))
|
| 24 |
+
SERVER_KEY = os.environ.get("HF_TOKEN", "")
|
| 25 |
+
|
| 26 |
+
app = FastAPI(title="DataInventor")
|
| 27 |
+
JOBS: dict[str, Job] = {}
|
| 28 |
+
ENDPOINTS: dict[str, Endpoint] = {} # kept apart from Job so a key never lands in a response
|
| 29 |
+
GUARD = threading.Lock()
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
class EngineIn(BaseModel):
|
| 33 |
+
base_url: str = HF_BASE
|
| 34 |
+
api_key: str = ""
|
| 35 |
+
model: str = HF_MODEL
|
| 36 |
+
disable_thinking: bool = True
|
| 37 |
+
workers: int = 4
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
class SpecIn(BaseModel):
|
| 41 |
+
engine: EngineIn = Field(default_factory=EngineIn)
|
| 42 |
+
format: str = "instruction"
|
| 43 |
+
request: str
|
| 44 |
+
domains: list[str] = []
|
| 45 |
+
subdomains: list[str] = []
|
| 46 |
+
languages: list[str] = ["English"]
|
| 47 |
+
constraints: str = ""
|
| 48 |
+
rows: int = 100
|
| 49 |
+
blueprint: dict | None = None
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def make_endpoint(e: EngineIn) -> Endpoint:
|
| 53 |
+
base = e.base_url.strip().rstrip("/")
|
| 54 |
+
if not base.startswith(("http://", "https://")):
|
| 55 |
+
raise HTTPException(400, "The base URL must start with http:// or https://.")
|
| 56 |
+
key = e.api_key.strip()
|
| 57 |
+
if not key:
|
| 58 |
+
if base == HF_BASE and SERVER_KEY:
|
| 59 |
+
key = SERVER_KEY # the hosted default only ever talks to the Hugging Face router
|
| 60 |
+
else:
|
| 61 |
+
raise HTTPException(400, "Add an API key for this endpoint.")
|
| 62 |
+
return Endpoint(base_url=base, api_key=key, model=e.model.strip() or HF_MODEL,
|
| 63 |
+
disable_thinking=e.disable_thinking, workers=max(1, min(e.workers, 8)))
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
@app.get("/api/defaults")
|
| 67 |
+
def defaults():
|
| 68 |
+
return {"base_url": HF_BASE, "model": HF_MODEL, "hosted_key": bool(SERVER_KEY), "max_rows": MAX_ROWS}
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
@app.get("/api/domains")
|
| 72 |
+
def domains():
|
| 73 |
+
return JSONResponse(json.loads((STATIC / "domains.json").read_text()))
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
@app.post("/api/ping")
|
| 77 |
+
def ping(e: EngineIn):
|
| 78 |
+
try:
|
| 79 |
+
return make_endpoint(e).ping()
|
| 80 |
+
except EndpointError as err:
|
| 81 |
+
raise HTTPException(502, str(err))
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
@app.post("/api/blueprint")
|
| 85 |
+
def blueprint(s: SpecIn):
|
| 86 |
+
ep = make_endpoint(s.engine)
|
| 87 |
+
if not s.request.strip():
|
| 88 |
+
raise HTTPException(400, "Describe the dataset first.")
|
| 89 |
+
try:
|
| 90 |
+
return make_blueprint(ep, s.request, s.domains, s.subdomains, s.languages, s.constraints, s.rows)
|
| 91 |
+
except EndpointError as err:
|
| 92 |
+
raise HTTPException(502, str(err))
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
@app.post("/api/jobs")
|
| 96 |
+
def start(s: SpecIn):
|
| 97 |
+
ep = make_endpoint(s.engine)
|
| 98 |
+
if not s.request.strip():
|
| 99 |
+
raise HTTPException(400, "Describe the dataset first.")
|
| 100 |
+
if not 1 <= s.rows <= MAX_ROWS:
|
| 101 |
+
raise HTTPException(400, f"Rows must be between 1 and {MAX_ROWS} on this host.")
|
| 102 |
+
with GUARD:
|
| 103 |
+
running = sum(1 for j in JOBS.values() if j.state in ("queued", "planning", "generating"))
|
| 104 |
+
if running >= MAX_RUNNING:
|
| 105 |
+
raise HTTPException(429, "Two datasets are already being written. Wait for one to finish.")
|
| 106 |
+
jid = uuid.uuid4().hex[:10]
|
| 107 |
+
cfg = s.model_dump(exclude={"engine"})
|
| 108 |
+
job = Job(id=jid, config=cfg)
|
| 109 |
+
JOBS[jid] = job
|
| 110 |
+
ENDPOINTS[jid] = ep
|
| 111 |
+
for old in sorted(JOBS, key=lambda k: JOBS[k].started)[:-20]:
|
| 112 |
+
JOBS.pop(old, None); ENDPOINTS.pop(old, None)
|
| 113 |
+
threading.Thread(target=run_job, args=(job, ep), daemon=True).start()
|
| 114 |
+
return {"id": jid}
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
def get_job(jid: str) -> Job:
|
| 118 |
+
if jid not in JOBS:
|
| 119 |
+
raise HTTPException(404, "That dataset is no longer here. Start a new run.")
|
| 120 |
+
return JOBS[jid]
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
@app.get("/api/jobs/{jid}")
|
| 124 |
+
def status(jid: str, since: int = 0):
|
| 125 |
+
j = get_job(jid)
|
| 126 |
+
with j.lock:
|
| 127 |
+
rows = list(j.rows)
|
| 128 |
+
out = {"id": j.id, "state": j.state, "message": j.message, "target": j.target, "count": len(rows), "attempts": j.attempts,
|
| 129 |
+
"rejected": dict(j.rejected), "elapsed": round((j.finished or time.time()) - j.started, 1), "new_rows": rows[since:since + 50]}
|
| 130 |
+
if j.state in ("done", "stopped", "failed"):
|
| 131 |
+
out["stats"] = stats(rows)
|
| 132 |
+
out["blueprint"] = j.blueprint
|
| 133 |
+
return out
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
@app.post("/api/jobs/{jid}/stop")
|
| 137 |
+
def stop(jid: str):
|
| 138 |
+
get_job(jid).stop.set()
|
| 139 |
+
return {"ok": True}
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
@app.get("/api/jobs/{jid}/download")
|
| 143 |
+
def download(jid: str, format: str = "csv"):
|
| 144 |
+
j = get_job(jid)
|
| 145 |
+
rows = list(j.rows)
|
| 146 |
+
if not rows:
|
| 147 |
+
raise HTTPException(404, "There are no rows to download yet.")
|
| 148 |
+
pref = "chosen" in rows[0]
|
| 149 |
+
cols = ["prompt", "chosen", "rejected"] if pref else ["prompt", "completion"]
|
| 150 |
+
meta = ["area", "subtopic", "topic", "persona", "shape", "difficulty", "language"]
|
| 151 |
+
name = f"datainventor-{jid}"
|
| 152 |
+
if format == "jsonl":
|
| 153 |
+
body = "\n".join(json.dumps({k: r.get(k) for k in cols + meta}, ensure_ascii=False) for r in rows)
|
| 154 |
+
return Response(body, media_type="application/jsonl", headers={"Content-Disposition": f'attachment; filename="{name}.jsonl"'})
|
| 155 |
+
buf = io.StringIO()
|
| 156 |
+
w = csv.writer(buf)
|
| 157 |
+
w.writerow(cols + meta)
|
| 158 |
+
for r in rows:
|
| 159 |
+
w.writerow([r.get(k, "") for k in cols + meta])
|
| 160 |
+
return Response(buf.getvalue().encode("utf-8-sig"), media_type="text/csv", headers={"Content-Disposition": f'attachment; filename="{name}.csv"'})
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
@app.get("/")
|
| 164 |
+
def index():
|
| 165 |
+
return FileResponse(STATIC / "index.html")
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
app.mount("/static", StaticFiles(directory=STATIC), name="static")
|
requirements.txt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi==0.115.*
|
| 2 |
+
uvicorn[standard]==0.32.*
|
| 3 |
+
pydantic>=2.7
|
static/app.css
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
:root {
|
| 2 |
+
--paper: #e7e8ea;
|
| 3 |
+
--paper-2: #f1f2f4;
|
| 4 |
+
--ink: #1d2036;
|
| 5 |
+
--blue: #3a4bff;
|
| 6 |
+
--pink: #ff5c8f;
|
| 7 |
+
--slate: #5d6376;
|
| 8 |
+
--rule: #c5c8d1;
|
| 9 |
+
--ok: #1f7a55;
|
| 10 |
+
--bad: #b3261e;
|
| 11 |
+
--ui: "Familjen Grotesk", system-ui, sans-serif;
|
| 12 |
+
--doc: "Literata", Georgia, serif;
|
| 13 |
+
--r: 3px;
|
| 14 |
+
}
|
| 15 |
+
* { box-sizing: border-box; }
|
| 16 |
+
html { color-scheme: light; }
|
| 17 |
+
body {
|
| 18 |
+
margin: 0; background: var(--paper); color: var(--ink);
|
| 19 |
+
font: 400 16px/1.5 var(--ui);
|
| 20 |
+
-webkit-font-smoothing: antialiased;
|
| 21 |
+
}
|
| 22 |
+
.skip { position: absolute; left: -999px; top: 0; background: var(--ink); color: #fff; padding: 8px 12px; z-index: 10; }
|
| 23 |
+
.skip:focus { left: 8px; top: 8px; }
|
| 24 |
+
:focus-visible { outline: 3px solid var(--blue); outline-offset: 2px; border-radius: var(--r); }
|
| 25 |
+
button, input, textarea, select { font: inherit; color: inherit; }
|
| 26 |
+
h1, h2, h3, p { margin: 0; }
|
| 27 |
+
|
| 28 |
+
/* top bar */
|
| 29 |
+
.top { display: flex; align-items: center; gap: 28px; padding: 22px 36px 18px; border-bottom: 1px solid var(--rule); }
|
| 30 |
+
.brand { display: flex; align-items: center; gap: 12px; flex: none; }
|
| 31 |
+
.mark { position: relative; width: 30px; height: 30px; display: inline-block; }
|
| 32 |
+
.mark i { position: absolute; inset: 0; border-radius: 50%; mix-blend-mode: multiply; }
|
| 33 |
+
.mark i:first-child { background: var(--blue); transform: translate(-3px, -2px); }
|
| 34 |
+
.mark i:last-child { background: var(--pink); transform: translate(4px, 3px); }
|
| 35 |
+
.name { font-size: 26px; font-weight: 700; letter-spacing: -0.02em; }
|
| 36 |
+
.tag { color: var(--slate); max-width: 62ch; font-size: 15px; }
|
| 37 |
+
|
| 38 |
+
/* layout */
|
| 39 |
+
.shell { display: grid; grid-template-columns: 210px minmax(0, 1fr) 330px; gap: 0; min-height: calc(100vh - 140px); }
|
| 40 |
+
.ledger { border-right: 1px solid var(--rule); padding: 26px 0 26px 36px; }
|
| 41 |
+
.panel { padding: 30px 44px 60px; min-width: 0; outline: none; }
|
| 42 |
+
.sheet { border-left: 1px solid var(--rule); padding: 30px 28px 40px; position: sticky; top: 0; align-self: start; max-height: 100vh; overflow: auto; }
|
| 43 |
+
|
| 44 |
+
/* ledger */
|
| 45 |
+
#steps { list-style: none; margin: 0; padding: 0; }
|
| 46 |
+
#steps li { position: relative; }
|
| 47 |
+
#steps button {
|
| 48 |
+
display: grid; grid-template-columns: 26px 1fr; align-items: baseline; gap: 6px; width: 100%;
|
| 49 |
+
text-align: left; background: none; border: 0; padding: 9px 12px 9px 0; cursor: pointer; color: var(--slate);
|
| 50 |
+
}
|
| 51 |
+
#steps .n { font-variant-numeric: tabular-nums; font-weight: 600; }
|
| 52 |
+
#steps li.done button { color: var(--ink); }
|
| 53 |
+
#steps li.done .n { color: var(--blue); }
|
| 54 |
+
#steps li.on button { color: var(--ink); font-weight: 600; }
|
| 55 |
+
#steps li.on::before { content: ""; position: absolute; left: -36px; top: 8px; bottom: 8px; width: 4px; background: var(--blue); }
|
| 56 |
+
#steps button:disabled { cursor: default; opacity: .55; }
|
| 57 |
+
|
| 58 |
+
/* type */
|
| 59 |
+
.panel h1 { font-size: clamp(30px, 4.2vw, 46px); line-height: 1.06; letter-spacing: -0.03em; font-weight: 700; max-width: 18ch; }
|
| 60 |
+
.panel h2 { font-size: 26px; line-height: 1.15; letter-spacing: -0.02em; font-weight: 700; max-width: 26ch; }
|
| 61 |
+
.lede { color: var(--slate); margin-top: 12px; max-width: 58ch; }
|
| 62 |
+
.group { margin-top: 30px; }
|
| 63 |
+
.group > h3 { font-size: 15px; font-weight: 600; margin-bottom: 4px; }
|
| 64 |
+
.hint { color: var(--slate); font-size: 14px; }
|
| 65 |
+
|
| 66 |
+
/* format specimens */
|
| 67 |
+
.specimens { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 22px; margin-top: 30px; }
|
| 68 |
+
.spec {
|
| 69 |
+
position: relative; text-align: left; background: var(--paper-2); border: 1px solid var(--rule);
|
| 70 |
+
border-radius: var(--r); padding: 20px 20px 22px; cursor: pointer;
|
| 71 |
+
}
|
| 72 |
+
.spec[aria-pressed="true"] { border-color: var(--ink); box-shadow: 5px 5px 0 -1px var(--blue), -3px -3px 0 -1px var(--pink); }
|
| 73 |
+
.spec b { display: block; font-size: 18px; margin-bottom: 4px; }
|
| 74 |
+
.spec .eg { font-family: var(--doc); font-size: 14.5px; line-height: 1.5; margin-top: 14px; color: #2b2f48; }
|
| 75 |
+
.spec .eg u { text-decoration: none; display: block; font-style: italic; color: var(--slate); margin-bottom: 2px; }
|
| 76 |
+
.spec .eg p + p { margin-top: 8px; }
|
| 77 |
+
|
| 78 |
+
/* choice lists */
|
| 79 |
+
.choices { list-style: none; margin: 14px 0 0; padding: 0; columns: 3 190px; column-gap: 28px; }
|
| 80 |
+
.choices li { break-inside: avoid; }
|
| 81 |
+
.choice {
|
| 82 |
+
display: grid; grid-template-columns: 18px 1fr; gap: 10px; align-items: start; width: 100%; text-align: left;
|
| 83 |
+
background: none; border: 0; padding: 7px 0; cursor: pointer;
|
| 84 |
+
}
|
| 85 |
+
.box { width: 16px; height: 16px; margin-top: 3px; border: 1.5px solid var(--ink); border-radius: 2px; background: var(--paper-2); display: grid; place-items: center; }
|
| 86 |
+
.choice[aria-pressed="true"] .box { background: var(--blue); border-color: var(--blue); }
|
| 87 |
+
.choice[aria-pressed="true"] .box::after { content: ""; width: 8px; height: 4px; border-left: 2px solid #fff; border-bottom: 2px solid #fff; transform: rotate(-45deg) translate(1px, -1px); }
|
| 88 |
+
.choice[aria-pressed="true"] span:last-child { font-weight: 600; }
|
| 89 |
+
.sub-block { margin-top: 26px; padding-top: 18px; border-top: 1px solid var(--rule); }
|
| 90 |
+
.sub-block h3 { font-size: 17px; }
|
| 91 |
+
|
| 92 |
+
/* fields */
|
| 93 |
+
label.f { display: block; margin-top: 22px; }
|
| 94 |
+
label.f > span { display: block; font-weight: 600; font-size: 15px; margin-bottom: 6px; }
|
| 95 |
+
.in, textarea.in, select.in {
|
| 96 |
+
width: 100%; background: var(--paper-2); border: 1px solid #aeb2bf; border-radius: var(--r); padding: 11px 13px;
|
| 97 |
+
}
|
| 98 |
+
textarea.in { min-height: 118px; resize: vertical; font-family: var(--doc); font-size: 16.5px; line-height: 1.55; }
|
| 99 |
+
.in:hover { border-color: var(--ink); }
|
| 100 |
+
.row2 { display: grid; grid-template-columns: 1fr 1fr; gap: 22px; }
|
| 101 |
+
.seg { display: inline-flex; flex-wrap: wrap; gap: 8px; margin-top: 8px; }
|
| 102 |
+
.seg button { background: var(--paper-2); border: 1px solid #aeb2bf; border-radius: var(--r); padding: 8px 15px; cursor: pointer; font-variant-numeric: tabular-nums; }
|
| 103 |
+
.seg button[aria-pressed="true"] { background: var(--ink); color: #fff; border-color: var(--ink); }
|
| 104 |
+
.check { display: flex; gap: 10px; align-items: flex-start; margin-top: 18px; }
|
| 105 |
+
.check input { margin-top: 5px; accent-color: var(--blue); width: 16px; height: 16px; }
|
| 106 |
+
|
| 107 |
+
/* buttons */
|
| 108 |
+
.actions { display: flex; flex-wrap: wrap; align-items: center; gap: 14px; margin-top: 36px; }
|
| 109 |
+
.btn { border: 1.5px solid var(--ink); background: var(--paper-2); border-radius: var(--r); padding: 11px 20px; font-weight: 600; cursor: pointer; }
|
| 110 |
+
.btn:hover:not(:disabled) { background: #fff; }
|
| 111 |
+
.btn.go { background: var(--blue); border-color: var(--blue); color: #fff; }
|
| 112 |
+
.btn.go:hover:not(:disabled) { background: #2a39e6; }
|
| 113 |
+
.btn:disabled { opacity: .45; cursor: not-allowed; }
|
| 114 |
+
.linkish { background: none; border: 0; padding: 0; color: var(--blue); font-weight: 600; cursor: pointer; text-decoration: underline; text-underline-offset: 3px; }
|
| 115 |
+
|
| 116 |
+
/* blueprint */
|
| 117 |
+
.bp { margin-top: 22px; border-top: 1px solid var(--ink); }
|
| 118 |
+
.bp-row { display: grid; grid-template-columns: 150px 1fr; gap: 18px; padding: 14px 0; border-bottom: 1px solid var(--rule); }
|
| 119 |
+
.bp-row > b { font-size: 14px; }
|
| 120 |
+
.bp ul { margin: 0; padding: 0; list-style: none; display: flex; flex-wrap: wrap; gap: 6px 8px; }
|
| 121 |
+
.bp li { background: var(--paper-2); border: 1px solid var(--rule); border-radius: 2px; padding: 3px 9px; font-size: 14px; }
|
| 122 |
+
.bp .tree { display: grid; gap: 8px; }
|
| 123 |
+
.bp .tree div > strong { font-weight: 600; }
|
| 124 |
+
.bp .tree small { color: var(--slate); }
|
| 125 |
+
.note { margin-top: 14px; padding: 10px 14px; border-left: 4px solid var(--pink); background: var(--paper-2); font-size: 14.5px; }
|
| 126 |
+
.note.bad { border-left-color: var(--bad); }
|
| 127 |
+
.note.ok { border-left-color: var(--ok); }
|
| 128 |
+
|
| 129 |
+
/* spec sheet */
|
| 130 |
+
.sheet h2 { font-size: 18px; margin-bottom: 14px; }
|
| 131 |
+
.sheet dl { margin: 0; }
|
| 132 |
+
.sheet .line { display: grid; grid-template-columns: 96px 1fr; gap: 10px; padding: 9px 0; border-top: 1px solid var(--rule); font-size: 14.5px; }
|
| 133 |
+
.sheet dt { color: var(--slate); }
|
| 134 |
+
.sheet dd { margin: 0; overflow-wrap: anywhere; }
|
| 135 |
+
.sheet .est { margin-top: 18px; font-size: 14.5px; color: var(--slate); }
|
| 136 |
+
.sheet .est b { color: var(--ink); }
|
| 137 |
+
|
| 138 |
+
/* run: the printed deck */
|
| 139 |
+
.count { display: flex; align-items: baseline; gap: 10px; margin-top: 24px; }
|
| 140 |
+
.count .big { font-size: clamp(54px, 9vw, 92px); line-height: 1; font-weight: 700; letter-spacing: -0.04em; font-variant-numeric: tabular-nums; }
|
| 141 |
+
.count .of { font-size: 22px; color: var(--slate); }
|
| 142 |
+
.bar { height: 6px; background: var(--rule); margin-top: 14px; border-radius: 3px; overflow: hidden; }
|
| 143 |
+
.bar > i { display: block; height: 100%; width: 0; background: var(--ink); transition: width .5s ease; }
|
| 144 |
+
.state { margin-top: 10px; color: var(--slate); }
|
| 145 |
+
.press { position: relative; height: 230px; margin: 30px 0 8px; }
|
| 146 |
+
.card {
|
| 147 |
+
position: absolute; left: 0; top: 0; width: min(100%, 640px); padding: 18px 22px 20px; background: #fbfbfc; border: 1px solid #aeb2bf; border-radius: 2px;
|
| 148 |
+
font-family: var(--doc); font-size: 15px; line-height: 1.5; overflow: hidden; height: 190px;
|
| 149 |
+
}
|
| 150 |
+
.card::before, .card::after { content: ""; position: absolute; inset: -1px; border-radius: 2px; pointer-events: none; mix-blend-mode: multiply; }
|
| 151 |
+
.card::before { box-shadow: 4px 3px 0 0 var(--pink); z-index: -1; }
|
| 152 |
+
.card::after { box-shadow: -3px -2px 0 0 var(--blue); z-index: -1; }
|
| 153 |
+
.card em { display: block; font-style: italic; color: var(--slate); font-size: 13px; margin-bottom: 3px; font-family: var(--ui); }
|
| 154 |
+
.card .a { margin-top: 10px; color: #3a3f5a; }
|
| 155 |
+
.card .clip { display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
|
| 156 |
+
.card.print { animation: print .45s cubic-bezier(.2,.8,.2,1) both; }
|
| 157 |
+
@keyframes print { from { transform: translate(14px, -10px); opacity: 0; } to { opacity: 1; } }
|
| 158 |
+
@media (prefers-reduced-motion: reduce) { .card.print { animation: none; } .bar > i { transition: none; } }
|
| 159 |
+
|
| 160 |
+
.table { margin-top: 28px; border-top: 1px solid var(--ink); }
|
| 161 |
+
.trow { display: grid; grid-template-columns: 44px minmax(0, 1fr); gap: 14px; padding: 12px 0; border-bottom: 1px solid var(--rule); }
|
| 162 |
+
.trow .id { color: var(--slate); font-variant-numeric: tabular-nums; }
|
| 163 |
+
.trow .p { font-family: var(--doc); }
|
| 164 |
+
.trow .meta { color: var(--slate); font-size: 13px; margin-top: 2px; }
|
| 165 |
+
.trow details { margin-top: 6px; }
|
| 166 |
+
.trow summary { cursor: pointer; color: var(--blue); font-weight: 600; font-size: 14px; }
|
| 167 |
+
.trow .full { font-family: var(--doc); white-space: pre-wrap; margin-top: 8px; font-size: 15px; line-height: 1.55; }
|
| 168 |
+
.stats { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 0; margin-top: 26px; border-top: 1px solid var(--ink); }
|
| 169 |
+
.stats div { padding: 14px 14px 14px 0; border-bottom: 1px solid var(--rule); }
|
| 170 |
+
.stats b { display: block; font-size: 24px; letter-spacing: -0.02em; font-variant-numeric: tabular-nums; }
|
| 171 |
+
.stats span { color: var(--slate); font-size: 13.5px; }
|
| 172 |
+
|
| 173 |
+
.foot { display: flex; justify-content: space-between; gap: 20px; padding: 18px 36px 26px; border-top: 1px solid var(--rule); color: var(--slate); font-size: 13.5px; flex-wrap: wrap; }
|
| 174 |
+
|
| 175 |
+
@media (max-width: 1100px) {
|
| 176 |
+
.shell { grid-template-columns: 1fr; }
|
| 177 |
+
.ledger { border: 0; border-bottom: 1px solid var(--rule); padding: 10px 20px; overflow-x: auto; }
|
| 178 |
+
#steps { display: flex; gap: 4px; }
|
| 179 |
+
#steps button { grid-template-columns: auto auto; white-space: nowrap; padding: 8px 12px; }
|
| 180 |
+
#steps li.on::before { left: 0; right: 0; top: auto; bottom: 0; height: 3px; width: auto; }
|
| 181 |
+
.sheet { position: static; border: 0; border-top: 1px solid var(--rule); max-height: none; }
|
| 182 |
+
.panel { padding: 24px 20px 40px; }
|
| 183 |
+
.top { padding: 18px 20px 14px; flex-wrap: wrap; gap: 8px 18px; }
|
| 184 |
+
}
|
| 185 |
+
@media (max-width: 640px) {
|
| 186 |
+
.specimens, .row2 { grid-template-columns: 1fr; }
|
| 187 |
+
.bp-row { grid-template-columns: 1fr; gap: 6px; }
|
| 188 |
+
.stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
| 189 |
+
.press { height: 250px; }
|
| 190 |
+
}
|
static/app.js
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use strict";
|
| 2 |
+
const STEPS = ["Format", "Domains", "Subdomains", "Languages", "Summary", "Engine", "Run"];
|
| 3 |
+
const LANGS = ["English", "Spanish", "French", "German", "Portuguese", "Italian", "Dutch", "Hindi", "Bengali", "Arabic", "Turkish", "Russian", "Polish", "Ukrainian",
|
| 4 |
+
"Chinese", "Japanese", "Korean", "Vietnamese", "Indonesian", "Thai", "Swahili", "Serbian", "Greek", "Hebrew"];
|
| 5 |
+
const PRESETS = {
|
| 6 |
+
hf: { label: "Hugging Face, free", base_url: "https://router.huggingface.co/v1", model: "prism-ml/Ternary-Bonsai-27B-AWQ-4bit:together", disable_thinking: true },
|
| 7 |
+
openai: { label: "OpenAI", base_url: "https://api.openai.com/v1", model: "gpt-4o-mini", disable_thinking: false },
|
| 8 |
+
local: { label: "Local server", base_url: "http://localhost:11434/v1", model: "llama3.1", disable_thinking: false },
|
| 9 |
+
};
|
| 10 |
+
const FORMATS = {
|
| 11 |
+
instruction: { name: "Instruction pairs", blurb: "A prompt and the reply you want a model to learn. Use it for supervised fine-tuning.",
|
| 12 |
+
eg: [["Prompt", "My hardcover arrived with a bent corner. Can I swap it?"], ["Completion", "Sorry about that. Yes, send a photo of the corner and we will ship a replacement today, no need to return the damaged copy."]] },
|
| 13 |
+
preference: { name: "Preference pairs", blurb: "A prompt with a chosen and a rejected reply, ranked by the model. Use it for preference tuning.",
|
| 14 |
+
eg: [["Prompt", "Where is order 8824? It was due Monday."], ["Chosen", "It left our warehouse Saturday and is with the courier. Tracking shows delivery Thursday."], ["Rejected", "Orders usually take a while. Please be patient."]] },
|
| 15 |
+
};
|
| 16 |
+
|
| 17 |
+
const S = {
|
| 18 |
+
step: 0, maxStep: 0, format: "instruction", domains: [], subs: {}, languages: ["English"], request: "", constraints: "", rows: 100,
|
| 19 |
+
blueprint: null, bpBusy: false, bpError: "", engine: { base_url: PRESETS.hf.base_url, model: PRESETS.hf.model, api_key: "", disable_thinking: true, workers: 4 },
|
| 20 |
+
preset: "hf", ping: null, pinging: false, job: null, jobRows: [], jobState: null, drawn: 0, printed: 0, hosted: false, maxRows: 1000, domainData: [], runError: "",
|
| 21 |
+
};
|
| 22 |
+
const $ = (s, el = document) => el.querySelector(s);
|
| 23 |
+
const esc = (t) => String(t ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));
|
| 24 |
+
const sentence = (t) => (t ? t.charAt(0).toUpperCase() + t.slice(1) : t);
|
| 25 |
+
|
| 26 |
+
async function api(path, body, method) {
|
| 27 |
+
const r = await fetch(path, { method: method || (body ? "POST" : "GET"), headers: body ? { "Content-Type": "application/json" } : {}, body: body ? JSON.stringify(body) : undefined });
|
| 28 |
+
let data = null; try { data = await r.json(); } catch (e) { /* not json */ }
|
| 29 |
+
if (!r.ok) throw new Error((data && (data.detail || data.message)) || `Request failed (${r.status})`);
|
| 30 |
+
return data;
|
| 31 |
+
}
|
| 32 |
+
const spec = () => ({ engine: S.engine, format: S.format, request: S.request.trim(), domains: S.domains, subdomains: Object.values(S.subs).flat(),
|
| 33 |
+
languages: S.languages, constraints: S.constraints, rows: S.rows, blueprint: S.blueprint });
|
| 34 |
+
|
| 35 |
+
/* ------------------------------------------------------------ chrome */
|
| 36 |
+
function renderLedger() {
|
| 37 |
+
$("#steps").innerHTML = STEPS.map((n, i) => {
|
| 38 |
+
const cls = i === S.step ? "on" : i < S.step ? "done" : "";
|
| 39 |
+
return `<li class="${cls}"><button data-go="${i}" ${i > S.maxStep ? "disabled" : ""} ${i === S.step ? 'aria-current="step"' : ""}><span class="n">${i + 1}</span><span>${n}</span></button></li>`;
|
| 40 |
+
}).join("");
|
| 41 |
+
}
|
| 42 |
+
function estimate() {
|
| 43 |
+
const per = S.format === "preference" ? 4.4 : 2.5, lat = (S.ping && S.ping.latency_ms) || 1800;
|
| 44 |
+
const calls = Math.round(S.rows * per + 2);
|
| 45 |
+
const secs = Math.max(10, Math.round((calls * lat) / 1000 / Math.max(1, S.engine.workers)));
|
| 46 |
+
const t = secs >= 3600 ? `${(secs / 3600).toFixed(1)} hours` : secs >= 90 ? `${Math.round(secs / 60)} minutes` : `${secs} seconds`;
|
| 47 |
+
return { calls, time: t };
|
| 48 |
+
}
|
| 49 |
+
function renderSheet() {
|
| 50 |
+
const e = estimate(), subs = Object.values(S.subs).flat();
|
| 51 |
+
const line = (k, v) => `<div class="line"><dt>${k}</dt><dd>${v}</dd></div>`;
|
| 52 |
+
$("#sheet").innerHTML = `<h2>Your dataset</h2><dl>
|
| 53 |
+
${line("Format", esc(FORMATS[S.format].name))}
|
| 54 |
+
${line("Domains", S.domains.length ? esc(S.domains.join(", ")) : '<span class="hint">None chosen. The description will guide it.</span>')}
|
| 55 |
+
${line("Subdomains", subs.length ? esc(subs.slice(0, 6).join(", ")) + (subs.length > 6 ? ` and ${subs.length - 6} more` : "") : '<span class="hint">All</span>')}
|
| 56 |
+
${line("Languages", esc(S.languages.join(", ")))}
|
| 57 |
+
${line("Rows", S.rows.toLocaleString())}
|
| 58 |
+
${line("Model", esc(S.engine.model))}
|
| 59 |
+
</dl><p class="est">About <b>${e.calls.toLocaleString()}</b> model calls and <b>${e.time}</b> on this setup.</p>`;
|
| 60 |
+
}
|
| 61 |
+
function go(i) { S.step = i; S.maxStep = Math.max(S.maxStep, i); render(); $("#panel").focus({ preventScroll: true }); window.scrollTo({ top: 0 }); }
|
| 62 |
+
function actions(back, nextLabel, nextOk = true, extra = "") {
|
| 63 |
+
return `<div class="actions">${back ? `<button class="btn" data-act="back">Back</button>` : ""}${nextLabel ? `<button class="btn go" data-act="next" ${nextOk ? "" : "disabled"}>${nextLabel}</button>` : ""}${extra}</div>`;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
/* ------------------------------------------------------------ steps */
|
| 67 |
+
function stepFormat() {
|
| 68 |
+
return `<h1>What kind of rows do you need?</h1>
|
| 69 |
+
<p class="lede">Pick the shape of each example. You can describe the content in the steps that follow.</p>
|
| 70 |
+
<div class="specimens">${Object.entries(FORMATS).map(([k, f]) => `
|
| 71 |
+
<button class="spec" data-format="${k}" aria-pressed="${S.format === k}"><b>${f.name}</b><span class="hint">${f.blurb}</span>
|
| 72 |
+
<div class="eg">${f.eg.map(([l, t]) => `<p><u>${l}</u>${esc(t)}</p>`).join("")}</div></button>`).join("")}</div>
|
| 73 |
+
${actions(false, "Continue")}`;
|
| 74 |
+
}
|
| 75 |
+
function stepDomains() {
|
| 76 |
+
return `<h2>Which areas should the data cover?</h2>
|
| 77 |
+
<p class="lede">Choose any number, or skip this and describe the data yourself in the summary.</p>
|
| 78 |
+
<ul class="choices">${S.domainData.map((d) => `<li><button class="choice" data-domain="${esc(d.name)}" aria-pressed="${S.domains.includes(d.name)}"><span class="box"></span><span>${esc(d.name)}</span></button></li>`).join("")}</ul>
|
| 79 |
+
${actions(true, "Continue")}`;
|
| 80 |
+
}
|
| 81 |
+
function stepSubs() {
|
| 82 |
+
if (!S.domains.length) return `<h2>Subdomains</h2><p class="lede">No domains chosen, so there is nothing to narrow. Continue to the languages, or go back and pick a domain.</p>${actions(true, "Continue")}`;
|
| 83 |
+
return `<h2>Narrow it down</h2><p class="lede">Pick the subdomains you care about. Leave a domain empty to cover all of it.</p>
|
| 84 |
+
${S.domains.map((name) => { const d = S.domainData.find((x) => x.name === name); const sel = S.subs[name] || [];
|
| 85 |
+
return `<div class="sub-block"><h3>${esc(name)}</h3><ul class="choices">${d.subdomains.map((s) => `<li><button class="choice" data-sub="${esc(name)}|${esc(s)}" aria-pressed="${sel.includes(s)}"><span class="box"></span><span>${esc(sentence(s))}</span></button></li>`).join("")}</ul></div>`; }).join("")}
|
| 86 |
+
${actions(true, "Continue")}`;
|
| 87 |
+
}
|
| 88 |
+
function stepLangs() {
|
| 89 |
+
return `<h2>Which languages?</h2><p class="lede">Rows are written directly in each language you choose, split evenly. English is the default.</p>
|
| 90 |
+
<ul class="choices">${LANGS.map((l) => `<li><button class="choice" data-lang="${l}" aria-pressed="${S.languages.includes(l)}"><span class="box"></span><span>${l}</span></button></li>`).join("")}</ul>
|
| 91 |
+
${actions(true, "Continue", S.languages.length > 0)}`;
|
| 92 |
+
}
|
| 93 |
+
function bpView() {
|
| 94 |
+
const bp = S.blueprint; if (!bp) return "";
|
| 95 |
+
return `<div class="bp">
|
| 96 |
+
<div class="bp-row"><b>Who answers</b><span>${esc(bp.assistant_role)}</span></div>
|
| 97 |
+
<div class="bp-row"><b>Topics</b><div class="tree">${bp.topics.map((a) => `<div><strong>${esc(a.area)}</strong> <small>${a.subtopics.map((s) => esc(s.subtopic)).join(", ")}</small></div>`).join("")}</div></div>
|
| 98 |
+
<div class="bp-row"><b>Who asks</b><ul>${bp.personas.map((p) => `<li>${esc(p)}</li>`).join("")}</ul></div>
|
| 99 |
+
<div class="bp-row"><b>Question shapes</b><ul>${bp.formats.map((p) => `<li>${esc(p)}</li>`).join("")}</ul></div>
|
| 100 |
+
</div>`;
|
| 101 |
+
}
|
| 102 |
+
function stepSummary() {
|
| 103 |
+
const suggest = () => { const d = S.domains.length ? ` covering ${S.domains.join(", ").toLowerCase()}` : ""; return `A dataset of ${S.format === "preference" ? "prompts with a better and a worse reply" : "prompts and ideal replies"}${d}.`; };
|
| 104 |
+
return `<h2>Describe the dataset</h2>
|
| 105 |
+
<p class="lede">Say what you want in your own words. The more specific you are about who asks and who answers, the better the rows.</p>
|
| 106 |
+
<label class="f"><span>What should the dataset contain?</span>
|
| 107 |
+
<textarea class="in" id="request" placeholder="Customer support conversations for a small online bookshop: order status, returns, damaged books and gift wrapping.">${esc(S.request)}</textarea></label>
|
| 108 |
+
<p style="margin-top:8px"><button class="linkish" data-act="suggest">Draft a description from my choices</button> <span class="hint" data-suggest="${esc(suggest())}"></span></p>
|
| 109 |
+
<label class="f"><span>Rules every row must follow <span class="hint">(optional)</span></span>
|
| 110 |
+
<textarea class="in" id="constraints" style="min-height:76px" placeholder="Replies must be under 120 words. Use plain text, no markdown.">${esc(S.constraints)}</textarea></label>
|
| 111 |
+
<div class="group"><h3>How many rows?</h3>
|
| 112 |
+
<div class="seg" role="group" aria-label="Rows">${[25, 100, 250, 500, 1000].filter((n) => n <= S.maxRows).map((n) => `<button data-rows="${n}" aria-pressed="${S.rows === n}">${n.toLocaleString()}</button>`).join("")}</div>
|
| 113 |
+
<p class="hint" style="margin-top:8px">Hosted runs are capped at ${S.maxRows.toLocaleString()} rows. Run it yourself for more.</p></div>
|
| 114 |
+
<div class="group"><h3>Blueprint</h3>
|
| 115 |
+
<p class="hint">A plan for coverage: topics, who asks, who answers, and the shapes questions take. Preview it before you spend model calls on rows.</p>
|
| 116 |
+
<div class="actions" style="margin-top:12px"><button class="btn" data-act="blueprint" ${S.bpBusy || !S.request.trim() ? "disabled" : ""}>${S.bpBusy ? "Drafting the blueprint" : S.blueprint ? "Draft it again" : "Preview the blueprint"}</button></div>
|
| 117 |
+
${S.bpError ? `<p class="note bad">${esc(S.bpError)}</p>` : ""}${bpView()}</div>
|
| 118 |
+
${actions(true, "Continue", !!S.request.trim())}`;
|
| 119 |
+
}
|
| 120 |
+
function stepEngine() {
|
| 121 |
+
const p = S.ping;
|
| 122 |
+
return `<h2>Choose the model that writes the rows</h2>
|
| 123 |
+
<p class="lede">DataInventor works with any OpenAI-compatible endpoint. The default is a free model on Hugging Face.</p>
|
| 124 |
+
<div class="seg" role="group" aria-label="Presets">${Object.entries(PRESETS).map(([k, v]) => `<button data-preset="${k}" aria-pressed="${S.preset === k}">${v.label}</button>`).join("")}</div>
|
| 125 |
+
<label class="f"><span>Base URL</span><input class="in" id="base_url" value="${esc(S.engine.base_url)}" spellcheck="false"></label>
|
| 126 |
+
<div class="row2"><label class="f"><span>Model</span><input class="in" id="model" value="${esc(S.engine.model)}" spellcheck="false"></label>
|
| 127 |
+
<label class="f"><span>API key</span><input class="in" id="api_key" type="password" autocomplete="off" placeholder="${S.preset === "hf" && S.hosted ? "Optional here. The host supplies one." : "Paste a key"}" value="${esc(S.engine.api_key)}"></label></div>
|
| 128 |
+
<p class="hint" style="margin-top:6px">The key is used for this run only. It is not stored.${S.preset === "local" ? " A hosted Space cannot reach localhost. Run DataInventor on your machine to use a local server." : ""}</p>
|
| 129 |
+
<label class="check"><input type="checkbox" id="think" ${S.engine.disable_thinking ? "checked" : ""}><span><b>Turn thinking off</b><br><span class="hint">For reasoning models that return empty text until they finish thinking. It makes each call several times faster. Leave it off for endpoints that reject the setting.</span></span></label>
|
| 130 |
+
<label class="f" style="max-width:240px"><span>Parallel calls</span><input class="in" id="workers" type="number" min="1" max="8" value="${S.engine.workers}"></label>
|
| 131 |
+
<div class="actions"><button class="btn" data-act="ping" ${S.pinging ? "disabled" : ""}>${S.pinging ? "Testing" : "Test the connection"}</button>
|
| 132 |
+
${p ? (p.ok ? `<span class="note ok" style="margin:0">Connected. One call took ${p.latency_ms} ms.</span>` : `<span class="note bad" style="margin:0">${esc(p.error)}</span>`) : ""}</div>
|
| 133 |
+
${actions(true, "Review and start")}`;
|
| 134 |
+
}
|
| 135 |
+
const isFinal = (j) => j && ["done", "stopped", "failed"].includes(j.state);
|
| 136 |
+
function rowHtml(r) {
|
| 137 |
+
const pref = S.format === "preference";
|
| 138 |
+
return `<div class="trow"><span class="id">${r.id}</span><div><div class="p">${esc(r.prompt)}</div><div class="meta">${esc(r.area)}, ${esc(r.persona)}${r.language && r.language !== "English" ? ", " + esc(r.language) : ""}</div>
|
| 139 |
+
<details><summary>${pref ? "Read both replies" : "Read the reply"}</summary><div class="full">${pref ? "Chosen\n" + esc(r.chosen) + "\n\nRejected\n" + esc(r.rejected) : esc(r.completion)}</div></details></div></div>`;
|
| 140 |
+
}
|
| 141 |
+
function deckHtml() {
|
| 142 |
+
const pref = S.format === "preference", latest = S.jobRows.slice(-3);
|
| 143 |
+
const fresh = latest.length ? latest[latest.length - 1].id : 0;
|
| 144 |
+
const out = latest.map((r, i) => `<div class="card ${i === latest.length - 1 && fresh > S.printed ? "print" : ""}" style="transform:translate(${i * 16}px, ${i * 14}px);z-index:${i}"><em>${esc(r.persona)}, ${esc(r.topic)}</em><div class="clip">${esc(r.prompt)}</div><div class="a clip">${esc(pref ? r.chosen : r.completion)}</div></div>`).join("");
|
| 145 |
+
S.printed = Math.max(S.printed, fresh);
|
| 146 |
+
return out;
|
| 147 |
+
}
|
| 148 |
+
function headParts() {
|
| 149 |
+
const j = S.jobState, n = j ? j.count : 0, t = j ? j.target || S.rows : S.rows, pct = t ? Math.min(100, Math.round((n / t) * 100)) : 0, fin = isFinal(j);
|
| 150 |
+
const st = j && j.stats && j.stats.rows ? j.stats : null;
|
| 151 |
+
return {
|
| 152 |
+
h2: fin ? (j.state === "failed" ? "The run stopped" : "Your dataset is ready") : "Writing your dataset",
|
| 153 |
+
count: `<span class="big">${n.toLocaleString()}</span><span class="of">of ${t.toLocaleString()} rows</span>`, pct,
|
| 154 |
+
state: `${j ? esc(j.message) : "Starting"}${j && !fin ? ` Elapsed ${Math.round(j.elapsed)} s.` : ""}`,
|
| 155 |
+
note: j && j.state === "failed" ? `<p class="note bad">${esc(j.message)}</p>` : j && j.state === "done" && j.count < j.target ? `<p class="note">${esc(j.message)}</p>` : "",
|
| 156 |
+
actions: fin ? `${j.count ? `<a class="btn go" href="/api/jobs/${j.id}/download?format=csv" style="text-decoration:none">Download CSV</a><a class="btn" href="/api/jobs/${j.id}/download?format=jsonl" style="text-decoration:none">Download JSONL</a>` : ""}<button class="btn" data-act="again">Start another</button>` : `<button class="btn" data-act="stop">Stop and keep what is written</button>`,
|
| 157 |
+
stats: st ? `<div class="stats"><div><b>${st.rows.toLocaleString()}</b><span>rows</span></div><div><b>${st.distinct3_prompts}</b><span>distinct 3-grams in prompts</span></div><div><b>${st.avg_prompt_words}</b><span>words per prompt</span></div><div><b>${st.avg_answer_words}</b><span>words per reply</span></div></div>` : "",
|
| 158 |
+
filtered: j && j.rejected && Object.keys(j.rejected).length ? `<p class="hint" style="margin-top:12px">Filtered out: ${Object.entries(j.rejected).map(([k, v]) => `${v} ${esc(k)}`).join(", ")}.</p>` : "",
|
| 159 |
+
};
|
| 160 |
+
}
|
| 161 |
+
function stepRun() {
|
| 162 |
+
if (S.runError && !S.jobState) return `<h2>The run did not start</h2><p class="note bad">${esc(S.runError)}</p>${actions(true, "Try again")}`;
|
| 163 |
+
const h = headParts(); S.drawn = S.jobRows.length;
|
| 164 |
+
return `<div id="runroot"><h2 id="run-h2">${h.h2}</h2>
|
| 165 |
+
<div class="count" id="run-count" aria-live="polite">${h.count}</div>
|
| 166 |
+
<div class="bar" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="${h.pct}"><i id="run-bar" style="width:${h.pct}%"></i></div>
|
| 167 |
+
<p class="state" id="run-state">${h.state}</p><div id="run-note">${h.note}</div>
|
| 168 |
+
<div class="actions" id="run-actions">${h.actions}</div><div id="run-stats">${h.stats}</div><div id="run-filtered">${h.filtered}</div>
|
| 169 |
+
<div class="press" id="run-deck" aria-hidden="true">${deckHtml()}</div>
|
| 170 |
+
<div class="table" id="run-rows">${S.jobRows.slice().reverse().map(rowHtml).join("")}</div></div>`;
|
| 171 |
+
}
|
| 172 |
+
/* patch the run view in place so open replies stay open and only a new card prints */
|
| 173 |
+
function updateRun() {
|
| 174 |
+
const h = headParts(), set = (id, html) => { const el = document.getElementById(id); if (el && el.innerHTML !== html) el.innerHTML = html; };
|
| 175 |
+
set("run-h2", h.h2); set("run-count", h.count); set("run-state", h.state); set("run-note", h.note); set("run-actions", h.actions); set("run-stats", h.stats); set("run-filtered", h.filtered);
|
| 176 |
+
const bar = document.getElementById("run-bar"); if (bar) { bar.style.width = h.pct + "%"; bar.parentElement.setAttribute("aria-valuenow", h.pct); }
|
| 177 |
+
const deck = document.getElementById("run-deck"); if (deck) deck.innerHTML = deckHtml();
|
| 178 |
+
const rows = document.getElementById("run-rows");
|
| 179 |
+
if (rows && S.jobRows.length > S.drawn) { rows.insertAdjacentHTML("afterbegin", S.jobRows.slice(S.drawn).reverse().map(rowHtml).join("")); S.drawn = S.jobRows.length; }
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
/* ------------------------------------------------------------ render + events */
|
| 183 |
+
function render() {
|
| 184 |
+
renderLedger(); renderSheet();
|
| 185 |
+
const fn = [stepFormat, stepDomains, stepSubs, stepLangs, stepSummary, stepEngine, stepRun][S.step];
|
| 186 |
+
const keep = document.activeElement && document.activeElement.id;
|
| 187 |
+
$("#panel").innerHTML = fn();
|
| 188 |
+
if (keep) { const el = document.getElementById(keep); if (el) { el.focus(); if (el.setSelectionRange && el.type !== "number") { const l = el.value.length; try { el.setSelectionRange(l, l); } catch (e) { /* ok */ } } } }
|
| 189 |
+
}
|
| 190 |
+
document.addEventListener("click", async (ev) => {
|
| 191 |
+
const t = ev.target.closest("button, a"); if (!t) return;
|
| 192 |
+
if (t.dataset.go !== undefined) return go(+t.dataset.go);
|
| 193 |
+
if (t.dataset.format) { S.format = t.dataset.format; S.blueprint = null; return render(); }
|
| 194 |
+
if (t.dataset.domain) { const d = t.dataset.domain; S.domains = S.domains.includes(d) ? S.domains.filter((x) => x !== d) : [...S.domains, d]; if (!S.domains.includes(d)) delete S.subs[d]; S.blueprint = null; return render(); }
|
| 195 |
+
if (t.dataset.sub) { const [d, s] = t.dataset.sub.split("|"); const cur = S.subs[d] || []; S.subs[d] = cur.includes(s) ? cur.filter((x) => x !== s) : [...cur, s]; S.blueprint = null; return render(); }
|
| 196 |
+
if (t.dataset.lang) { const l = t.dataset.lang; S.languages = S.languages.includes(l) ? S.languages.filter((x) => x !== l) : [...S.languages, l]; S.blueprint = null; return render(); }
|
| 197 |
+
if (t.dataset.rows) { S.rows = +t.dataset.rows; return render(); }
|
| 198 |
+
if (t.dataset.preset) { const p = PRESETS[t.dataset.preset]; S.preset = t.dataset.preset; S.engine = { ...S.engine, base_url: p.base_url, model: p.model, disable_thinking: p.disable_thinking }; S.ping = null; return render(); }
|
| 199 |
+
const act = t.dataset.act; if (!act) return;
|
| 200 |
+
if (act === "next") return void go(S.step + 1);
|
| 201 |
+
if (act === "back") return go(Math.max(0, S.step - 1));
|
| 202 |
+
if (act === "suggest") { S.request = S.request.trim() ? S.request : $("[data-suggest]").dataset.suggest; return render(); }
|
| 203 |
+
if (act === "blueprint") return doBlueprint();
|
| 204 |
+
if (act === "ping") return doPing();
|
| 205 |
+
if (act === "stop") { if (S.job) await api(`/api/jobs/${S.job}/stop`, {}); return; }
|
| 206 |
+
if (act === "again") { S.job = null; S.jobState = null; S.jobRows = []; S.drawn = 0; S.printed = 0; S.runError = ""; return go(4); }
|
| 207 |
+
});
|
| 208 |
+
document.addEventListener("input", (ev) => {
|
| 209 |
+
const id = ev.target.id;
|
| 210 |
+
if (id === "request") { S.request = ev.target.value; S.blueprint = null; const b = $('[data-act="next"]'); if (b) b.disabled = !S.request.trim(); const bp = $('[data-act="blueprint"]'); if (bp) bp.disabled = S.bpBusy || !S.request.trim(); }
|
| 211 |
+
else if (id === "constraints") { S.constraints = ev.target.value; S.blueprint = null; }
|
| 212 |
+
else if (id === "base_url") { S.engine.base_url = ev.target.value; S.preset = ""; }
|
| 213 |
+
else if (id === "model") S.engine.model = ev.target.value;
|
| 214 |
+
else if (id === "api_key") S.engine.api_key = ev.target.value;
|
| 215 |
+
else if (id === "workers") { S.engine.workers = Math.max(1, Math.min(8, +ev.target.value || 1)); renderSheet(); }
|
| 216 |
+
else if (id === "think") S.engine.disable_thinking = ev.target.checked;
|
| 217 |
+
if (id === "model") renderSheet();
|
| 218 |
+
});
|
| 219 |
+
document.addEventListener("change", (ev) => { if (ev.target.id === "think") S.engine.disable_thinking = ev.target.checked; });
|
| 220 |
+
|
| 221 |
+
async function doBlueprint() {
|
| 222 |
+
S.bpBusy = true; S.bpError = ""; render();
|
| 223 |
+
try { S.blueprint = await api("/api/blueprint", spec()); } catch (e) { S.bpError = e.message; }
|
| 224 |
+
S.bpBusy = false; render();
|
| 225 |
+
}
|
| 226 |
+
async function doPing() {
|
| 227 |
+
S.pinging = true; S.ping = null; render();
|
| 228 |
+
try { S.ping = await api("/api/ping", S.engine); } catch (e) { S.ping = { ok: false, error: e.message }; }
|
| 229 |
+
S.pinging = false; render();
|
| 230 |
+
}
|
| 231 |
+
async function startRun() {
|
| 232 |
+
S.runError = ""; S.jobRows = []; S.jobState = null; S.drawn = 0; S.printed = 0;
|
| 233 |
+
try { const r = await api("/api/jobs", spec()); S.job = r.id; poll(); } catch (e) { S.runError = e.message; }
|
| 234 |
+
render();
|
| 235 |
+
}
|
| 236 |
+
async function poll() {
|
| 237 |
+
const id = S.job; if (!id) return;
|
| 238 |
+
try {
|
| 239 |
+
const j = await api(`/api/jobs/${id}?since=${S.jobRows.length}`);
|
| 240 |
+
if (id !== S.job) return;
|
| 241 |
+
const wasFinal = isFinal(S.jobState); S.jobRows.push(...j.new_rows); S.jobState = j;
|
| 242 |
+
if (S.step === 6) { if (document.getElementById("runroot") && !wasFinal) updateRun(); else render(); }
|
| 243 |
+
if (!["done", "stopped", "failed"].includes(j.state) || j.count > S.jobRows.length) return void setTimeout(poll, j.count > S.jobRows.length ? 200 : 1500);
|
| 244 |
+
if (j.stats && j.stats.rows) S.maxStep = 6;
|
| 245 |
+
} catch (e) { console.error(e.stack); S.jobState = { state: "failed", message: e.message, count: S.jobRows.length, target: S.rows, elapsed: 0, id }; if (S.step === 6) render(); }
|
| 246 |
+
}
|
| 247 |
+
/* "Review and start" on the engine step starts the run */
|
| 248 |
+
const _go = go;
|
| 249 |
+
go = function (i) { const wasEngine = S.step === 5; _go(i); if (i === 6 && wasEngine && !S.jobState) startRun(); return true; };
|
| 250 |
+
|
| 251 |
+
(async function init() {
|
| 252 |
+
try { const d = await api("/api/defaults"); S.hosted = d.hosted_key; S.maxRows = d.max_rows; if (S.rows > S.maxRows) S.rows = S.maxRows; $("#hostnote").textContent = d.hosted_key ? "Hosted model key supplied by the Space." : ""; } catch (e) { /* offline */ }
|
| 253 |
+
try { S.domainData = await api("/api/domains"); } catch (e) { S.domainData = []; }
|
| 254 |
+
render();
|
| 255 |
+
})();
|
static/domains.json
ADDED
|
@@ -0,0 +1,716 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"name": "Academic education",
|
| 4 |
+
"subdomains": [
|
| 5 |
+
"online learning",
|
| 6 |
+
"virtual classrooms",
|
| 7 |
+
"open educational resources",
|
| 8 |
+
"blended learning",
|
| 9 |
+
"microlearning",
|
| 10 |
+
"flipped classroom",
|
| 11 |
+
"adaptive learning",
|
| 12 |
+
"peer tutoring",
|
| 13 |
+
"competency based education",
|
| 14 |
+
"digital literacy",
|
| 15 |
+
"collaborative learning",
|
| 16 |
+
"personalized education"
|
| 17 |
+
]
|
| 18 |
+
},
|
| 19 |
+
{
|
| 20 |
+
"name": "Agriculture",
|
| 21 |
+
"subdomains": [
|
| 22 |
+
"Precision Farming",
|
| 23 |
+
"Sustainable Agriculture",
|
| 24 |
+
"Agroecology",
|
| 25 |
+
"Vertical Farming",
|
| 26 |
+
"Regenerative Agriculture",
|
| 27 |
+
"Organic Farming",
|
| 28 |
+
"Aquaculture",
|
| 29 |
+
"Permaculture",
|
| 30 |
+
"Agroforestry",
|
| 31 |
+
"Hydroponics",
|
| 32 |
+
"Biotechnology in Agriculture",
|
| 33 |
+
"Climate-Smart Agriculture"
|
| 34 |
+
]
|
| 35 |
+
},
|
| 36 |
+
{
|
| 37 |
+
"name": "Animal and nature",
|
| 38 |
+
"subdomains": [
|
| 39 |
+
"Wildlife Conservation",
|
| 40 |
+
"Botanical Gardens",
|
| 41 |
+
"Marine Biology",
|
| 42 |
+
"Bird Watching",
|
| 43 |
+
"Insect Ecology",
|
| 44 |
+
"Forest Preservation",
|
| 45 |
+
"Zoo Management",
|
| 46 |
+
"Nature Photography",
|
| 47 |
+
"Habitat Restoration",
|
| 48 |
+
"Mammal Studies",
|
| 49 |
+
"Desert Ecosystems",
|
| 50 |
+
"Aquatic Life"
|
| 51 |
+
]
|
| 52 |
+
},
|
| 53 |
+
{
|
| 54 |
+
"name": "Architecture and design",
|
| 55 |
+
"subdomains": [
|
| 56 |
+
"residential architecture",
|
| 57 |
+
"commercial design",
|
| 58 |
+
"sustainable building",
|
| 59 |
+
"urban planning",
|
| 60 |
+
"interior design",
|
| 61 |
+
"landscape architecture",
|
| 62 |
+
"industrial design",
|
| 63 |
+
"digital architecture",
|
| 64 |
+
"adaptive reuse",
|
| 65 |
+
"smart cities",
|
| 66 |
+
"biophilic design",
|
| 67 |
+
"modular construction"
|
| 68 |
+
]
|
| 69 |
+
},
|
| 70 |
+
{
|
| 71 |
+
"name": "Art",
|
| 72 |
+
"subdomains": [
|
| 73 |
+
"Art History",
|
| 74 |
+
"Modern Art",
|
| 75 |
+
"Contemporary Art",
|
| 76 |
+
"Digital Art",
|
| 77 |
+
"Abstract Art",
|
| 78 |
+
"Sculpture Art",
|
| 79 |
+
"Photography Art",
|
| 80 |
+
"Street Art",
|
| 81 |
+
"Pop Art",
|
| 82 |
+
"Minimalist Art",
|
| 83 |
+
"Installation Art",
|
| 84 |
+
"Folk Art"
|
| 85 |
+
]
|
| 86 |
+
},
|
| 87 |
+
{
|
| 88 |
+
"name": "Career and workplace",
|
| 89 |
+
"subdomains": [
|
| 90 |
+
"Resume Writing",
|
| 91 |
+
"Interview Skills",
|
| 92 |
+
"Career Planning",
|
| 93 |
+
"Workplace Communication",
|
| 94 |
+
"Professional Development",
|
| 95 |
+
"Leadership Training",
|
| 96 |
+
"Time Management",
|
| 97 |
+
"Conflict Resolution",
|
| 98 |
+
"Team Collaboration",
|
| 99 |
+
"Job Search Strategies",
|
| 100 |
+
"Salary Negotiation",
|
| 101 |
+
"Remote Work Etiquette"
|
| 102 |
+
]
|
| 103 |
+
},
|
| 104 |
+
{
|
| 105 |
+
"name": "Code",
|
| 106 |
+
"subdomains": [
|
| 107 |
+
"Code Review",
|
| 108 |
+
"Code Analysis",
|
| 109 |
+
"Code Optimization",
|
| 110 |
+
"Code Refactoring",
|
| 111 |
+
"Code Documentation",
|
| 112 |
+
"Code Testing",
|
| 113 |
+
"Code Security",
|
| 114 |
+
"Code Generation",
|
| 115 |
+
"Code Quality",
|
| 116 |
+
"Code Architecture",
|
| 117 |
+
"Code Maintenance",
|
| 118 |
+
"Code Education"
|
| 119 |
+
]
|
| 120 |
+
},
|
| 121 |
+
{
|
| 122 |
+
"name": "Cooking",
|
| 123 |
+
"subdomains": [
|
| 124 |
+
"quick recipes",
|
| 125 |
+
"healthy meals",
|
| 126 |
+
"baking basics",
|
| 127 |
+
"vegan cooking",
|
| 128 |
+
"meal prep",
|
| 129 |
+
"dessert ideas",
|
| 130 |
+
"kitchen tips",
|
| 131 |
+
"international cuisine",
|
| 132 |
+
"slow cooker",
|
| 133 |
+
"budget cooking",
|
| 134 |
+
"food safety",
|
| 135 |
+
"pasta dishes"
|
| 136 |
+
]
|
| 137 |
+
},
|
| 138 |
+
{
|
| 139 |
+
"name": "Corporate and business",
|
| 140 |
+
"subdomains": [
|
| 141 |
+
"Corporate Governance",
|
| 142 |
+
"Business Strategy",
|
| 143 |
+
"Financial Management",
|
| 144 |
+
"Human Resources",
|
| 145 |
+
"Marketing Analytics",
|
| 146 |
+
"Supply Chain",
|
| 147 |
+
"Risk Management",
|
| 148 |
+
"Customer Service",
|
| 149 |
+
"Legal Compliance",
|
| 150 |
+
"Project Management",
|
| 151 |
+
"Leadership Development",
|
| 152 |
+
"Operational Efficiency"
|
| 153 |
+
]
|
| 154 |
+
},
|
| 155 |
+
{
|
| 156 |
+
"name": "Culture",
|
| 157 |
+
"subdomains": [
|
| 158 |
+
"Visual Arts",
|
| 159 |
+
"Music History",
|
| 160 |
+
"Literary Traditions",
|
| 161 |
+
"Performing Arts",
|
| 162 |
+
"Cultural Anthropology",
|
| 163 |
+
"Digital Culture",
|
| 164 |
+
"Food Culture",
|
| 165 |
+
"Religious Practices",
|
| 166 |
+
"Fashion Trends",
|
| 167 |
+
"Architectural Heritage",
|
| 168 |
+
"Folklore Studies",
|
| 169 |
+
"Media Studies"
|
| 170 |
+
]
|
| 171 |
+
},
|
| 172 |
+
{
|
| 173 |
+
"name": "Data analysis and visualization",
|
| 174 |
+
"subdomains": [
|
| 175 |
+
"Data Cleaning Techniques",
|
| 176 |
+
"Statistical Analysis Methods",
|
| 177 |
+
"Machine Learning Algorithms",
|
| 178 |
+
"Data Visualization Tools",
|
| 179 |
+
"Database Management Systems",
|
| 180 |
+
"Big Data Processing",
|
| 181 |
+
"Predictive Analytics Models",
|
| 182 |
+
"Exploratory Data Analysis",
|
| 183 |
+
"Data Mining Strategies",
|
| 184 |
+
"Information Visualization Design",
|
| 185 |
+
"Natural Language Processing",
|
| 186 |
+
"Real-time Data Streaming"
|
| 187 |
+
]
|
| 188 |
+
},
|
| 189 |
+
{
|
| 190 |
+
"name": "Dating",
|
| 191 |
+
"subdomains": [
|
| 192 |
+
"dating apps",
|
| 193 |
+
"online dating",
|
| 194 |
+
"dating sites",
|
| 195 |
+
"dating advice",
|
| 196 |
+
"dating tips",
|
| 197 |
+
"dating culture",
|
| 198 |
+
"dating trends",
|
| 199 |
+
"dating psychology",
|
| 200 |
+
"dating safety",
|
| 201 |
+
"dating etiquette",
|
| 202 |
+
"dating apps",
|
| 203 |
+
"dating strategies"
|
| 204 |
+
]
|
| 205 |
+
},
|
| 206 |
+
{
|
| 207 |
+
"name": "Entertainment",
|
| 208 |
+
"subdomains": [
|
| 209 |
+
"Movie Reviews",
|
| 210 |
+
"TV Series",
|
| 211 |
+
"Music Charts",
|
| 212 |
+
"Celebrity News",
|
| 213 |
+
"Streaming Platforms",
|
| 214 |
+
"Concert Tickets",
|
| 215 |
+
"Theme Parks",
|
| 216 |
+
"Podcast Shows",
|
| 217 |
+
"Video Games",
|
| 218 |
+
"Live Events",
|
| 219 |
+
"Book Adaptations",
|
| 220 |
+
"Comedy Specials"
|
| 221 |
+
]
|
| 222 |
+
},
|
| 223 |
+
{
|
| 224 |
+
"name": "Fashion and beauty",
|
| 225 |
+
"subdomains": [
|
| 226 |
+
"Skincare Basics",
|
| 227 |
+
"Makeup Tutorials",
|
| 228 |
+
"Hairstyle Ideas",
|
| 229 |
+
"Fashion Trends",
|
| 230 |
+
"Beauty Products",
|
| 231 |
+
"Style Guides",
|
| 232 |
+
"Wellness Tips",
|
| 233 |
+
"Beauty Hacks",
|
| 234 |
+
"Fashion History",
|
| 235 |
+
"Skincare Routines",
|
| 236 |
+
"Makeup Reviews",
|
| 237 |
+
"Fashion Inspiration"
|
| 238 |
+
]
|
| 239 |
+
},
|
| 240 |
+
{
|
| 241 |
+
"name": "Fitness and sports",
|
| 242 |
+
"subdomains": [
|
| 243 |
+
"home fitness",
|
| 244 |
+
"outdoor sports",
|
| 245 |
+
"team sports",
|
| 246 |
+
"individual training",
|
| 247 |
+
"strength training",
|
| 248 |
+
"cardiovascular fitness",
|
| 249 |
+
"yoga and pilates",
|
| 250 |
+
"martial arts",
|
| 251 |
+
"swimming fitness",
|
| 252 |
+
"cycling sports",
|
| 253 |
+
"running training",
|
| 254 |
+
"flexibility exercises"
|
| 255 |
+
]
|
| 256 |
+
},
|
| 257 |
+
{
|
| 258 |
+
"name": "Games",
|
| 259 |
+
"subdomains": [
|
| 260 |
+
"mobile games",
|
| 261 |
+
"indie games",
|
| 262 |
+
"racing games",
|
| 263 |
+
"strategy games",
|
| 264 |
+
"role playing games",
|
| 265 |
+
"shooter games",
|
| 266 |
+
"puzzle games",
|
| 267 |
+
"sports games",
|
| 268 |
+
"simulation games",
|
| 269 |
+
"board games",
|
| 270 |
+
"card games",
|
| 271 |
+
"party games"
|
| 272 |
+
]
|
| 273 |
+
},
|
| 274 |
+
{
|
| 275 |
+
"name": "Geography",
|
| 276 |
+
"subdomains": [
|
| 277 |
+
"Physical Geography",
|
| 278 |
+
"Human Geography",
|
| 279 |
+
"Cartography",
|
| 280 |
+
"Geomorphology",
|
| 281 |
+
"Biogeography",
|
| 282 |
+
"Economic Geography",
|
| 283 |
+
"Political Geography",
|
| 284 |
+
"Cultural Geography",
|
| 285 |
+
"Urban Geography",
|
| 286 |
+
"Environmental Geography",
|
| 287 |
+
"Social Geography",
|
| 288 |
+
"Historical Geography"
|
| 289 |
+
]
|
| 290 |
+
},
|
| 291 |
+
{
|
| 292 |
+
"name": "Governance",
|
| 293 |
+
"subdomains": [
|
| 294 |
+
"Public Administration",
|
| 295 |
+
"Corporate Governance",
|
| 296 |
+
"Government Accountability",
|
| 297 |
+
"Policy Implementation",
|
| 298 |
+
"Regulatory Compliance",
|
| 299 |
+
"Ethical Leadership",
|
| 300 |
+
"Stakeholder Engagement",
|
| 301 |
+
"Decision Making",
|
| 302 |
+
"Organizational Strategy",
|
| 303 |
+
"Transparency Practices",
|
| 304 |
+
"Governance Frameworks",
|
| 305 |
+
"Accountability Mechanisms"
|
| 306 |
+
]
|
| 307 |
+
},
|
| 308 |
+
{
|
| 309 |
+
"name": "History",
|
| 310 |
+
"subdomains": [
|
| 311 |
+
"Ancient Civilizations",
|
| 312 |
+
"Medieval Europe",
|
| 313 |
+
"World War II",
|
| 314 |
+
"Cold War Era",
|
| 315 |
+
"American Revolution",
|
| 316 |
+
"Renaissance Period",
|
| 317 |
+
"Industrial Revolution",
|
| 318 |
+
"Civil Rights Movement",
|
| 319 |
+
"Ancient Egypt",
|
| 320 |
+
"Classical Greece",
|
| 321 |
+
"Cold War Diplomacy",
|
| 322 |
+
"Colonial America"
|
| 323 |
+
]
|
| 324 |
+
},
|
| 325 |
+
{
|
| 326 |
+
"name": "How-to",
|
| 327 |
+
"subdomains": [
|
| 328 |
+
"how-to-fix",
|
| 329 |
+
"how-to-cook",
|
| 330 |
+
"how-to-program",
|
| 331 |
+
"how-to-travel",
|
| 332 |
+
"how-to-clean",
|
| 333 |
+
"how-to-build",
|
| 334 |
+
"how-to-save",
|
| 335 |
+
"how-to-learn",
|
| 336 |
+
"how-to-organize",
|
| 337 |
+
"how-to-design",
|
| 338 |
+
"how-to-market",
|
| 339 |
+
"how-to-lead"
|
| 340 |
+
]
|
| 341 |
+
},
|
| 342 |
+
{
|
| 343 |
+
"name": "HR",
|
| 344 |
+
"subdomains": [
|
| 345 |
+
"Human Resources",
|
| 346 |
+
"Recruitment Services",
|
| 347 |
+
"Employee Relations",
|
| 348 |
+
"Performance Management",
|
| 349 |
+
"Compensation Benefits",
|
| 350 |
+
"Training Development",
|
| 351 |
+
"Organizational Development",
|
| 352 |
+
"Workforce Planning",
|
| 353 |
+
"Employee Engagement",
|
| 354 |
+
"HR Analytics",
|
| 355 |
+
"Compliance Legal",
|
| 356 |
+
"Diversity Inclusion"
|
| 357 |
+
]
|
| 358 |
+
},
|
| 359 |
+
{
|
| 360 |
+
"name": "Language",
|
| 361 |
+
"subdomains": [
|
| 362 |
+
"Natural Language Processing",
|
| 363 |
+
"Computational Linguistics",
|
| 364 |
+
"Machine Translation",
|
| 365 |
+
"Speech Recognition",
|
| 366 |
+
"Text Generation",
|
| 367 |
+
"Sentiment Analysis",
|
| 368 |
+
"Machine Learning",
|
| 369 |
+
"Deep Learning",
|
| 370 |
+
"Neural Networks",
|
| 371 |
+
"Data Annotation",
|
| 372 |
+
"Language Modeling",
|
| 373 |
+
"Natural Language Inference"
|
| 374 |
+
]
|
| 375 |
+
},
|
| 376 |
+
{
|
| 377 |
+
"name": "Legal",
|
| 378 |
+
"subdomains": [
|
| 379 |
+
"legal research",
|
| 380 |
+
"case law",
|
| 381 |
+
"statutory interpretation",
|
| 382 |
+
"legal ethics",
|
| 383 |
+
"contract law",
|
| 384 |
+
"tort law",
|
| 385 |
+
"criminal procedure",
|
| 386 |
+
"civil procedure",
|
| 387 |
+
"legal writing",
|
| 388 |
+
"legal argumentation",
|
| 389 |
+
"legal reasoning",
|
| 390 |
+
"legal ethics"
|
| 391 |
+
]
|
| 392 |
+
},
|
| 393 |
+
{
|
| 394 |
+
"name": "Market analysis",
|
| 395 |
+
"subdomains": [
|
| 396 |
+
"Market Trends Analysis",
|
| 397 |
+
"Consumer Behavior Study",
|
| 398 |
+
"Competitive Intelligence Report",
|
| 399 |
+
"Industry Growth Forecast",
|
| 400 |
+
"Product Market Fit",
|
| 401 |
+
"Market Segmentation Strategy",
|
| 402 |
+
"Demand Supply Analysis",
|
| 403 |
+
"Pricing Strategy Review",
|
| 404 |
+
"Brand Positioning Analysis",
|
| 405 |
+
"Customer Acquisition Cost",
|
| 406 |
+
"Market Share Evaluation",
|
| 407 |
+
"Economic Impact Study"
|
| 408 |
+
]
|
| 409 |
+
},
|
| 410 |
+
{
|
| 411 |
+
"name": "Marketing",
|
| 412 |
+
"subdomains": [
|
| 413 |
+
"Digital Marketing",
|
| 414 |
+
"Content Marketing",
|
| 415 |
+
"Social Media Marketing",
|
| 416 |
+
"Email Marketing",
|
| 417 |
+
"Search Engine Marketing",
|
| 418 |
+
"Performance Marketing",
|
| 419 |
+
"Brand Marketing",
|
| 420 |
+
"Growth Marketing",
|
| 421 |
+
"Content Strategy",
|
| 422 |
+
"Marketing Automation",
|
| 423 |
+
"Marketing Analytics",
|
| 424 |
+
"Influencer Marketing"
|
| 425 |
+
]
|
| 426 |
+
},
|
| 427 |
+
{
|
| 428 |
+
"name": "Math",
|
| 429 |
+
"subdomains": [
|
| 430 |
+
"Linear Algebra",
|
| 431 |
+
"Calculus",
|
| 432 |
+
"Statistics",
|
| 433 |
+
"Number Theory",
|
| 434 |
+
"Topology",
|
| 435 |
+
"Probability",
|
| 436 |
+
"Differential Equations",
|
| 437 |
+
"Abstract Algebra",
|
| 438 |
+
"Geometry",
|
| 439 |
+
"Combinatorics",
|
| 440 |
+
"Analysis",
|
| 441 |
+
"Graph Theory"
|
| 442 |
+
]
|
| 443 |
+
},
|
| 444 |
+
{
|
| 445 |
+
"name": "Medical",
|
| 446 |
+
"subdomains": [
|
| 447 |
+
"medical research",
|
| 448 |
+
"clinical trials",
|
| 449 |
+
"medical education",
|
| 450 |
+
"medical devices",
|
| 451 |
+
"medical imaging",
|
| 452 |
+
"medical diagnostics",
|
| 453 |
+
"medical treatments",
|
| 454 |
+
"medical ethics",
|
| 455 |
+
"medical statistics",
|
| 456 |
+
"medical informatics",
|
| 457 |
+
"medical genetics",
|
| 458 |
+
"medical psychology"
|
| 459 |
+
]
|
| 460 |
+
},
|
| 461 |
+
{
|
| 462 |
+
"name": "Music",
|
| 463 |
+
"subdomains": [
|
| 464 |
+
"Music Theory Basics",
|
| 465 |
+
"Jazz Harmony Guide",
|
| 466 |
+
"Classical Music History",
|
| 467 |
+
"Electronic Music Production",
|
| 468 |
+
"Songwriting Techniques",
|
| 469 |
+
"Music Performance Skills",
|
| 470 |
+
"Music Technology Tools",
|
| 471 |
+
"Music Education Methods",
|
| 472 |
+
"Music Psychology Insights",
|
| 473 |
+
"Music Marketing Strategies",
|
| 474 |
+
"Music Copyright Laws",
|
| 475 |
+
"Music Streaming Platforms"
|
| 476 |
+
]
|
| 477 |
+
},
|
| 478 |
+
{
|
| 479 |
+
"name": "News",
|
| 480 |
+
"subdomains": [
|
| 481 |
+
"Breaking News",
|
| 482 |
+
"World News",
|
| 483 |
+
"Sports News",
|
| 484 |
+
"Tech News",
|
| 485 |
+
"Business News",
|
| 486 |
+
"Health News",
|
| 487 |
+
"Entertainment News",
|
| 488 |
+
"Local News",
|
| 489 |
+
"Science News",
|
| 490 |
+
"Political News",
|
| 491 |
+
"Climate News",
|
| 492 |
+
"Opinion News"
|
| 493 |
+
]
|
| 494 |
+
},
|
| 495 |
+
{
|
| 496 |
+
"name": "Parenting and family",
|
| 497 |
+
"subdomains": [
|
| 498 |
+
"Child Development Stages",
|
| 499 |
+
"Family Communication Skills",
|
| 500 |
+
"Parenting Styles Overview",
|
| 501 |
+
"Emotional Intelligence Training",
|
| 502 |
+
"Conflict Resolution Techniques",
|
| 503 |
+
"Positive Discipline Methods",
|
| 504 |
+
"Teen Parenting Challenges",
|
| 505 |
+
"Single Parent Support",
|
| 506 |
+
"Blended Family Dynamics",
|
| 507 |
+
"Early Childhood Education",
|
| 508 |
+
"Family Rituals and Traditions",
|
| 509 |
+
"Parental Self-Care Practices"
|
| 510 |
+
]
|
| 511 |
+
},
|
| 512 |
+
{
|
| 513 |
+
"name": "Personal finance",
|
| 514 |
+
"subdomains": [
|
| 515 |
+
"budgeting basics",
|
| 516 |
+
"debt management",
|
| 517 |
+
"investment strategies",
|
| 518 |
+
"retirement planning",
|
| 519 |
+
"tax optimization",
|
| 520 |
+
"credit score improvement",
|
| 521 |
+
"financial literacy",
|
| 522 |
+
"emergency fund building",
|
| 523 |
+
"real estate investing",
|
| 524 |
+
"cryptocurrency basics",
|
| 525 |
+
"insurance coverage",
|
| 526 |
+
"financial independence"
|
| 527 |
+
]
|
| 528 |
+
},
|
| 529 |
+
{
|
| 530 |
+
"name": "Personal growth",
|
| 531 |
+
"subdomains": [
|
| 532 |
+
"Mindset Mastery",
|
| 533 |
+
"Habit Formation",
|
| 534 |
+
"Emotional Intelligence",
|
| 535 |
+
"Goal Setting",
|
| 536 |
+
"Self Awareness",
|
| 537 |
+
"Productivity Systems",
|
| 538 |
+
"Communication Skills",
|
| 539 |
+
"Resilience Building",
|
| 540 |
+
"Time Management",
|
| 541 |
+
"Confidence Development",
|
| 542 |
+
"Critical Thinking",
|
| 543 |
+
"Work Life Balance"
|
| 544 |
+
]
|
| 545 |
+
},
|
| 546 |
+
{
|
| 547 |
+
"name": "Product advice",
|
| 548 |
+
"subdomains": [
|
| 549 |
+
"Product Advice",
|
| 550 |
+
"Product Reviews",
|
| 551 |
+
"Product Comparison",
|
| 552 |
+
"Product Ratings",
|
| 553 |
+
"Product Specs",
|
| 554 |
+
"Product Deals",
|
| 555 |
+
"Product Tips",
|
| 556 |
+
"Product Guides",
|
| 557 |
+
"Product Q&A",
|
| 558 |
+
"Product News",
|
| 559 |
+
"Product Updates",
|
| 560 |
+
"Product Support"
|
| 561 |
+
]
|
| 562 |
+
},
|
| 563 |
+
{
|
| 564 |
+
"name": "Religion",
|
| 565 |
+
"subdomains": [
|
| 566 |
+
"Christian Theology",
|
| 567 |
+
"Islamic Jurisprudence",
|
| 568 |
+
"Buddhist Philosophy",
|
| 569 |
+
"Jewish Law",
|
| 570 |
+
"Hindu Mythology",
|
| 571 |
+
"Sikh Beliefs",
|
| 572 |
+
"Daoist Practices",
|
| 573 |
+
"Zoroastrianism",
|
| 574 |
+
"Jainism",
|
| 575 |
+
"Secular Humanism",
|
| 576 |
+
"Comparative Religion",
|
| 577 |
+
"Religious Ethics"
|
| 578 |
+
]
|
| 579 |
+
},
|
| 580 |
+
{
|
| 581 |
+
"name": "Roleplay",
|
| 582 |
+
"subdomains": [
|
| 583 |
+
"Creative Writing Roleplay",
|
| 584 |
+
"Fantasy Roleplay Community",
|
| 585 |
+
"Sci-Fi Roleplay Hub",
|
| 586 |
+
"Historical Roleplay Forum",
|
| 587 |
+
"Romance Roleplay Network",
|
| 588 |
+
"Horror Roleplay Group",
|
| 589 |
+
"Mystery Roleplay Space",
|
| 590 |
+
"Adventure Roleplay Zone",
|
| 591 |
+
"Daily Roleplay Challenges",
|
| 592 |
+
"Roleplay Tips and Tricks",
|
| 593 |
+
"Beginner Roleplay Guide",
|
| 594 |
+
"Advanced Roleplay Techniques"
|
| 595 |
+
]
|
| 596 |
+
},
|
| 597 |
+
{
|
| 598 |
+
"name": "Science",
|
| 599 |
+
"subdomains": [
|
| 600 |
+
"Space Exploration",
|
| 601 |
+
"Quantum Physics",
|
| 602 |
+
"Marine Biology",
|
| 603 |
+
"Climate Science",
|
| 604 |
+
"Neuroscience Research",
|
| 605 |
+
"Astrophysics Studies",
|
| 606 |
+
"Genetics Analysis",
|
| 607 |
+
"Chemistry Education",
|
| 608 |
+
"Environmental Science",
|
| 609 |
+
"Mathematics Theory",
|
| 610 |
+
"Robotics Development",
|
| 611 |
+
"Virology Studies"
|
| 612 |
+
]
|
| 613 |
+
},
|
| 614 |
+
{
|
| 615 |
+
"name": "Social media",
|
| 616 |
+
"subdomains": [
|
| 617 |
+
"Social Networking",
|
| 618 |
+
"Content Sharing",
|
| 619 |
+
"Video Streaming",
|
| 620 |
+
"Micro Blogging",
|
| 621 |
+
"Photo Sharing",
|
| 622 |
+
"Audio Podcasting",
|
| 623 |
+
"Professional Networking",
|
| 624 |
+
"Community Forums",
|
| 625 |
+
"Live Streaming",
|
| 626 |
+
"Text Messaging",
|
| 627 |
+
"Photo Editing",
|
| 628 |
+
"Social Commerce"
|
| 629 |
+
]
|
| 630 |
+
},
|
| 631 |
+
{
|
| 632 |
+
"name": "Sports",
|
| 633 |
+
"subdomains": [
|
| 634 |
+
"Olympic Games",
|
| 635 |
+
"Professional Leagues",
|
| 636 |
+
"College Athletics",
|
| 637 |
+
"Youth Sports",
|
| 638 |
+
"Extreme Sports",
|
| 639 |
+
"Adaptive Sports",
|
| 640 |
+
"Esports",
|
| 641 |
+
"Martial Arts",
|
| 642 |
+
"Winter Sports",
|
| 643 |
+
"Outdoor Adventures",
|
| 644 |
+
"Fitness Training",
|
| 645 |
+
"Sports Psychology"
|
| 646 |
+
]
|
| 647 |
+
},
|
| 648 |
+
{
|
| 649 |
+
"name": "Technology",
|
| 650 |
+
"subdomains": [
|
| 651 |
+
"Artificial Intelligence",
|
| 652 |
+
"Cybersecurity",
|
| 653 |
+
"Cloud Computing",
|
| 654 |
+
"Data Science",
|
| 655 |
+
"Internet of Things",
|
| 656 |
+
"Machine Learning",
|
| 657 |
+
"Mobile Development",
|
| 658 |
+
"Robotics",
|
| 659 |
+
"Software Engineering",
|
| 660 |
+
"Virtual Reality",
|
| 661 |
+
"Web Development",
|
| 662 |
+
"Blockchain Technology"
|
| 663 |
+
]
|
| 664 |
+
},
|
| 665 |
+
{
|
| 666 |
+
"name": "Transportation",
|
| 667 |
+
"subdomains": [
|
| 668 |
+
"Public Transit Systems",
|
| 669 |
+
"Freight Logistics Management",
|
| 670 |
+
"Urban Mobility Solutions",
|
| 671 |
+
"Railway Network Operations",
|
| 672 |
+
"Aviation Safety Protocols",
|
| 673 |
+
"Maritime Shipping Routes",
|
| 674 |
+
"Electric Vehicle Infrastructure",
|
| 675 |
+
"Traffic Flow Optimization",
|
| 676 |
+
"Autonomous Vehicle Testing",
|
| 677 |
+
"Pedestrian Safety Measures",
|
| 678 |
+
"Cycling Infrastructure Development",
|
| 679 |
+
"Supply Chain Coordination"
|
| 680 |
+
]
|
| 681 |
+
},
|
| 682 |
+
{
|
| 683 |
+
"name": "Travel",
|
| 684 |
+
"subdomains": [
|
| 685 |
+
"budget travel",
|
| 686 |
+
"luxury travel",
|
| 687 |
+
"adventure travel",
|
| 688 |
+
"eco travel",
|
| 689 |
+
"solo travel",
|
| 690 |
+
"family travel",
|
| 691 |
+
"cruise travel",
|
| 692 |
+
"road trip",
|
| 693 |
+
"backpacking travel",
|
| 694 |
+
"cultural travel",
|
| 695 |
+
"business travel",
|
| 696 |
+
"beach travel"
|
| 697 |
+
]
|
| 698 |
+
},
|
| 699 |
+
{
|
| 700 |
+
"name": "Writing and editing",
|
| 701 |
+
"subdomains": [
|
| 702 |
+
"style guides",
|
| 703 |
+
"grammar rules",
|
| 704 |
+
"punctuation usage",
|
| 705 |
+
"sentence structure",
|
| 706 |
+
"paragraph organization",
|
| 707 |
+
"narrative techniques",
|
| 708 |
+
"academic writing",
|
| 709 |
+
"business correspondence",
|
| 710 |
+
"creative storytelling",
|
| 711 |
+
"technical documentation",
|
| 712 |
+
"editing strategies",
|
| 713 |
+
"proofreading methods"
|
| 714 |
+
]
|
| 715 |
+
}
|
| 716 |
+
]
|
static/index.html
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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">
|
| 6 |
+
<title>DataInventor</title>
|
| 7 |
+
<meta name="description" content="Describe a dataset in plain words and get training data back. Open source, works with any OpenAI-compatible model.">
|
| 8 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 9 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 10 |
+
<link href="https://fonts.googleapis.com/css2?family=Familjen+Grotesk:ital,wght@0,400..700;1,400..700&family=Literata:ital,opsz,wght@0,7..72,400..600;1,7..72,400..600&display=swap" rel="stylesheet">
|
| 11 |
+
<link rel="stylesheet" href="/static/app.css">
|
| 12 |
+
</head>
|
| 13 |
+
<body>
|
| 14 |
+
<a class="skip" href="#panel">Skip to the step</a>
|
| 15 |
+
<header class="top">
|
| 16 |
+
<div class="brand">
|
| 17 |
+
<span class="mark" aria-hidden="true"><i></i><i></i></span>
|
| 18 |
+
<span class="name">DataInventor</span>
|
| 19 |
+
</div>
|
| 20 |
+
<p class="tag">Describe the data you wish existed. Get training rows back. Bring any OpenAI-compatible model.</p>
|
| 21 |
+
</header>
|
| 22 |
+
|
| 23 |
+
<main class="shell">
|
| 24 |
+
<nav class="ledger" aria-label="Steps"><ol id="steps"></ol></nav>
|
| 25 |
+
<section class="panel" id="panel" tabindex="-1" aria-live="polite"></section>
|
| 26 |
+
<aside class="sheet" id="sheet" aria-label="Your dataset so far"></aside>
|
| 27 |
+
</main>
|
| 28 |
+
|
| 29 |
+
<footer class="foot">
|
| 30 |
+
<span>Open source recreation of the Invent a Dataset workflow. Not affiliated with Adaption Labs.</span>
|
| 31 |
+
<span id="hostnote"></span>
|
| 32 |
+
</footer>
|
| 33 |
+
<script src="/static/app.js"></script>
|
| 34 |
+
</body>
|
| 35 |
+
</html>
|