add batch
This commit is contained in:
@@ -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}")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user