add batch

This commit is contained in:
Mariano Gabriel
2026-06-26 11:38:45 -03:00
parent 3c96de85ad
commit 5ea05eb553
4 changed files with 410 additions and 53 deletions

View File

@@ -42,53 +42,30 @@ DEFAULT_BASE_URL = "http://localhost:11000/v1"
DEFAULT_MODEL = "Qwen/Qwen2.5-VL-7B-Instruct-AWQ"
CHARS_PER_TOKEN = 4.0
GROUNDING = """\
Rules:
- Be faithful. Never invent names, components, commands, numbers, or steps.
- Preserve proper nouns and identifiers exactly as written.
- This is a COMPILATION, not a summary: keep technical detail (workflows step by
step, architecture components and how they connect, configs, commands, gotchas).
- Reorganize by TOPIC, not by conversation order. Merge new info into the right
existing section rather than appending chronologically.
- Anchor concrete items to the [mm:ss] where they were said/shown.
- ASR vigilance on TERMS, especially acronyms and product/tool names: this
transcript is machine-transcribed, so a term that reads oddly or makes no sense
in context is likely a mis-hearing (a slightly-off acronym, a homophone, a
split or merged word). Flag it like "(heard: X — likely Y?)", using context to
infer the intended term; never silently propagate a nonsensical token, and
never silently "correct" a term you are unsure about.
- If something is unclear or only partially stated, mark it (e.g. "(unclear)")
rather than guessing."""
REFINE_SYS = """\
You maintain a growing TECHNICAL REFERENCE compiled from a training recording.
The user's compilation instruction is authoritative:
You are building up a DETAILED REFERENCE DOCUMENT from a meeting/training
transcript, one window at a time. The user's intent — follow it; otherwise use
your own judgment for structure, depth, ordering, and emphasis:
<instruction>
{instruction}
</instruction>
You are given the CURRENT REFERENCE so far and the NEXT WINDOW of transcript
(and possibly some screen frames). Integrate any new workflow/architecture detail
from this window into the reference, slotting it into the correct topical section
(create sections as needed). Return the COMPLETE updated reference in Markdown —
not a diff, not just the new part. Do not drop earlier content.
{rules}"""
You get the document so far and the next window of transcript (sometimes with
screen frames). Fold the new material into the document and return the COMPLETE
updated document, dropping nothing important from before. Keep concrete detail —
names, numbers, steps, configs, specifics — rather than collapsing to general
ideas; this is a reference, not a recap. Stay faithful to the transcript and
don't invent. It's machine-transcribed, so use your own judgment on garbled
spots (an odd acronym is probably a mis-hearing). Beyond that, write and organize
it however reads best to you."""
TRIAGE_SYS = """\
You are reading one window of a training transcript while compiling technical
notes per this instruction:
<instruction>
{instruction}
</instruction>
The window references the screen frames listed below (id + [mm:ss]). Decide which
frames you would need to SEE to capture workflow/architecture/config detail the
text alone doesn't convey (diagrams, slides, terminal output, code). Ignore
webcam/transition frames. Reply with STRICT JSON only:
You're compiling a detailed reference and reading this window of transcript. The
frames listed below are screenshots referenced in it (id + [mm:ss]). List the ids
of any you'd find worth actually looking at — lean toward looking whenever a frame
might carry detail the words alone don't. Reply with JSON only:
{{"need": ["<frame-id>", ...]}}
Empty list if none are needed."""
Empty list if none seem useful."""
FRAME_RE = re.compile(r"Frame:\s+(\S+\.(?:jpg|jpeg|png))", re.IGNORECASE)
TS_RE = re.compile(r"\[(\d+):(\d+)\]")
@@ -98,13 +75,15 @@ def estimate_tokens(text):
return int(len(text) / CHARS_PER_TOKEN)
def default_output(transcript, kind):
"""Write next to the transcript, in the same run folder, following the
pipeline's <stem>_<kind> naming (e.g. training_reference.md)."""
def default_output(transcript, kind, base_dir=None):
"""Write into base_dir (default: next to the transcript, in the same run
folder), following the pipeline's <stem>_<kind> naming (e.g.
training_reference.md)."""
stem = transcript.stem
if stem.endswith("_enhanced"):
stem = stem[: -len("_enhanced")]
return transcript.parent / f"{stem}_{kind}.md"
base = base_dir or transcript.parent
return base / f"{stem}_{kind}.md"
def content_tokens(content):
@@ -183,12 +162,15 @@ def encode_image(path, max_side):
return f"data:{mime};base64,{b64}"
def make_client(base_url, api_key):
def make_client(base_url, api_key, timeout):
try:
from openai import OpenAI
except ImportError:
sys.exit("ERROR: `openai` not installed here. Run under ~/wdir/llm/.venv")
return OpenAI(base_url=base_url, api_key=api_key)
# timeout=None => no limit (a slow mixed GPU/CPU model can take >>10min per call;
# the client default of ~600s is what raised RequestTimedOut). max_retries=0 so a
# rare hiccup doesn't silently re-send a 40-minute generation.
return OpenAI(base_url=base_url, api_key=api_key, timeout=timeout, max_retries=0)
def call(client, model, system, content, temperature, max_tokens):
@@ -201,6 +183,30 @@ def call(client, model, system, content, temperature, max_tokens):
return resp.choices[0].message.content.strip()
def call_fit(client, model, system, content, temperature, ctx, init_out):
"""Like call(), but bulletproof against token-estimate error: if the server
rejects the request for exceeding context, parse the REAL input-token count
from its error and retry with an exactly-fitting output budget. Re-raises the
context error only when the input alone fills the window (the doc-too-big
case the caller handles by stopping)."""
out = init_out
last = None
for _ in range(4):
try:
return call(client, model, system, content, temperature, out)
except Exception as e:
last = e
m = re.search(r"at least (\d+) input tokens", str(e))
if "maximum context length" in str(e) and m:
real_in = int(m.group(1))
out = ctx - real_in - 64
if out < 256:
break # input itself ~fills the window — genuine overflow
continue
raise
raise last
def parse_need(raw, valid_ids):
raw = re.sub(r"^```(?:json)?|```$", "", raw.strip(), flags=re.MULTILINE)
m = re.search(r"\{.*\}", raw, re.DOTALL)
@@ -221,6 +227,10 @@ def main():
p.add_argument("instruction", nargs="?",
default="Compile a detailed technical reference of the workflows and architecture covered.")
p.add_argument("-o", "--output", type=Path, help="write here (default: <run>/<stem>_reference.md next to the transcript)")
p.add_argument("--output-dir", type=Path,
help="base/parent directory; the run folder (taken from the "
"transcript's folder name) is auto-created under it and the "
"output written inside (default: the transcript's folder)")
p.add_argument("--stdout", action="store_true", help="print to stdout instead of writing a file")
p.add_argument("--base-url", default=DEFAULT_BASE_URL)
p.add_argument("--model", default=DEFAULT_MODEL)
@@ -231,6 +241,7 @@ def main():
p.add_argument("--ctx", type=int, default=16384, help="model context window; output is auto-capped so input+output fit (default 16384)")
p.add_argument("--max-image-side", type=int, default=1280, help="downscale frames to this max side (0=off)")
p.add_argument("--temperature", type=float, default=0.2)
p.add_argument("--timeout", type=float, default=0, help="per-request timeout in seconds; 0 = no limit (default, for slow local models)")
p.add_argument("--checkpoint", type=Path, help="write the running doc here after each window (resumable progress)")
p.add_argument("-q", "--quiet", action="store_true")
args = p.parse_args()
@@ -246,7 +257,7 @@ def main():
nframes = sum(len(w["frames"]) for w in windows)
log(f"{len(windows)} windows, {nframes} frame refs, mode={args.frames}")
client = make_client(args.base_url, args.api_key)
client = make_client(args.base_url, args.api_key, args.timeout or None)
doc = "# (compilation in progress)\n"
for wi, w in enumerate(windows, 1):
@@ -280,13 +291,22 @@ def main():
content = text
log(f" window {wi}/{len(windows)}: text-only")
sys_txt = REFINE_SYS.format(instruction=args.instruction, rules=GROUNDING)
sys_txt = REFINE_SYS.format(instruction=args.instruction)
in_tok = content_tokens(content) + estimate_tokens(sys_txt)
out_budget = max(512, min(args.max_tokens, args.ctx - in_tok - 256))
if in_tok > args.ctx - 512:
log(f" WARNING: doc+window ~{in_tok} tok ≥ ctx {args.ctx}; output will truncate — "
f"use a 32k profile (qwen14b-gguf) or lower --window-tokens for the full training")
doc = call(client, args.model, sys_txt, content, args.temperature, out_budget)
# proportional margin absorbs token-estimate error; call_fit self-corrects
# from the server's real count if it's still off.
out_budget = max(256, min(args.max_tokens, args.ctx - in_tok - max(512, in_tok // 20)))
try:
doc = call_fit(client, args.model, sys_txt, content,
args.temperature, args.ctx, out_budget)
except Exception as e:
if "maximum context length" in str(e):
log(f" STOP at window {wi}/{len(windows)}: the running doc filled the "
f"{args.ctx}-token window. Partial reference is in the checkpoint. A {args.ctx // 1024}k "
f"refine cannot hold a whole long-meeting doc — use the chunk-at-breaks + "
f"compact-carry + merge design, or a 32k-context profile.")
break
raise
if args.checkpoint:
args.checkpoint.write_text(doc + "\n")
@@ -297,7 +317,16 @@ def main():
if args.stdout:
print(doc)
else:
out = args.output or default_output(args.transcript, "reference")
# --output-dir is the PARENT; auto-create the run folder under it (named
# after the transcript's own run folder), matching process_meeting.py.
base_dir = (args.output_dir / args.transcript.parent.name) if args.output_dir else args.transcript.parent
if args.output:
out = args.output
if not out.is_absolute():
out = base_dir / out
else:
out = default_output(args.transcript, "reference", base_dir)
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(doc + "\n")
log(f"wrote {out}")