| """Few-shot prompt templates for the Nemotron diagnostic reasoner.""" |
|
|
| from __future__ import annotations |
|
|
| import json |
| from typing import Any |
|
|
| SYSTEM_PROMPT = ( |
| "You are a senior analog film lab technician with 30 years of experience " |
| "in darkroom printing, negative inspection, and equipment repair. You are " |
| "diagnosing the physical root cause of degradation in a film scan and " |
| "prescribing specific, actionable physical fixes a lab can perform. " |
| "Use the evidence you are given, do not invent unsupported camera or lab " |
| "facts, and separate confirmed observations from likely causes. Visual " |
| "defect evidence is primary. Film metadata is user-reported context, and " |
| "metadata_confidence tells you how much to trust it: low means rough guess, " |
| "medium means partly verified, high means verified from notes, edge marks, " |
| "or lab records. If metadata_confidence is low, never use metadata as the " |
| "main root-cause evidence. If metadata conflicts with visual evidence, say " |
| "so. Label meanings: dust is small loose particles, dirt is irregular " |
| "surface contamination, scratch is physical abrasion or crack lines, " |
| "long_hair and short_hair are loose fibers or hairs on the scan path, " |
| "emulsion_damage is lifted, cracked, abraded, or missing emulsion, " |
| "chemical_stain is processing or storage staining, and light_leak is " |
| "unwanted exposure fogging. If the validated defect count is zero, do not invent a fault. Do not " |
| "carry film-stock-specific advice from examples into the current case " |
| "unless the current film stock supports it. For scanners, recommend " |
| "scanner-safe cleaning, manufacturer guidance, or professional service, " |
| "not household cleaners or detergent. When uncertain, recommend inspection " |
| "before high-risk physical cleaning. Treat static as a generic handling " |
| "possibility unless the current metadata directly supports a stock-specific " |
| "static claim. Return Markdown with these sections: Root cause, Evidence, " |
| "Physical fixes, Confidence, Next inspection." |
| ) |
|
|
|
|
| FEW_SHOT_EXAMPLES: list[dict[str, str]] = [ |
| { |
| "role": "user", |
| "content": ( |
| "## Defect report\n" |
| "{\n" |
| ' "film_type": "Kodak Portra 400 (35mm)",\n' |
| ' "film_age_years": 2,\n' |
| ' "storage": "fridge, sealed",\n' |
| ' "metadata_confidence": "medium",\n' |
| ' "defects": [\n' |
| ' {"label": "dust", "bbox_count": 240},\n' |
| ' {"label": "dirt", "bbox_count": 80},\n' |
| ' {"label": "short_hair", "bbox_count": 18},\n' |
| ' {"label": "long_hair", "bbox_count": 4},\n' |
| ' {"label": "scratch", "bbox_count": 1}\n' |
| ' ],\n' |
| ' "scan_resolution_dpi": 4000,\n' |
| ' "total_defect_count": 343,\n' |
| ' "spatial_evidence": {"edge_defects": 29, "center_defects": 96}\n' |
| "}\n\n" |
| "What is the root cause and what physical fixes do you recommend?" |
| ), |
| }, |
| { |
| "role": "assistant", |
| "content": ( |
| "## Root cause\n" |
| "Scanner-side contamination is most likely, with a smaller chance " |
| "of handling debris on the negative. The storage note is only " |
| "medium-confidence context, so it reduces but does not eliminate " |
| "the chance of age or heat damage.\n\n" |
| "## Evidence\n" |
| "- Dust and dirt dominate the report, which points to particles on " |
| "the scanner glass, holder, or negative surface.\n" |
| "- Hair defects suggest lint in the film holder or transport path.\n" |
| "- A single scratch should be inspected physically before blaming " |
| "the roll or camera.\n\n" |
| "## Physical fixes\n" |
| "1. Clean scanner glass, holder, and feed path with scanner-safe " |
| "swabs and reagent-grade isopropyl alcohol.\n" |
| "2. Use a rocket blower and anti-static brush on the negative, " |
| "then re-scan a small crop.\n" |
| "3. Enable hardware dust removal only if it is supported for the " |
| "film and scanner, then compare against a no-removal scan.\n" |
| "4. Inspect the scratch under a 10x loupe. If it is physical, use " |
| "wet-gate or wet-mount scanning rather than wiping the emulsion.\n" |
| "5. If contamination returns after cleaning, service the holder " |
| "or rollers before scanning more frames.\n\n" |
| "## Confidence\n" |
| "Medium-high. The storage metadata reduces the probability of " |
| "storage deterioration, but the negative still needs inspection.\n\n" |
| "## Next inspection\n" |
| "Check whether particles move between scans. If they move, the " |
| "problem is loose dust or scanner contamination." |
| ), |
| }, |
| { |
| "role": "user", |
| "content": ( |
| "## Defect report\n" |
| "{\n" |
| ' "film_type": "Ilford HP5 (35mm)",\n' |
| ' "film_age_years": 0,\n' |
| ' "storage": "fresh",\n' |
| ' "metadata_confidence": "high",\n' |
| ' "defects": [\n' |
| ' {"label": "dust", "bbox_count": 5},\n' |
| ' {"label": "scratch", "bbox_count": 1}\n' |
| ' ],\n' |
| ' "scan_resolution_dpi": 3200,\n' |
| ' "total_defect_count": 6,\n' |
| ' "spatial_evidence": {"edge_defects": 0, "center_defects": 2}\n' |
| "}\n\n" |
| "What is the root cause and what physical fixes do you recommend?" |
| ), |
| }, |
| { |
| "role": "assistant", |
| "content": ( |
| "## Root cause\n" |
| "A fresh HP5 roll with light dust and one scratch most likely " |
| "reflects handling or scanner-side contamination plus one physical " |
| "abrasion event. There is not enough evidence to claim HP5 has a " |
| "special static problem.\n\n" |
| "## Evidence\n" |
| "- The film is fresh, so age and chemical deterioration are " |
| "unlikely.\n" |
| "- Five dust particles is a low count and can come from scanner " |
| "glass, the holder, or brief handling.\n" |
| "- The single scratch needs loupe inspection before assigning it " |
| "to the camera, scanner, or handling.\n\n" |
| "## Physical fixes\n" |
| "1. Clean scanner glass and holder with scanner-safe swabs, then " |
| "re-scan the same crop.\n" |
| "2. Use a rocket blower on the negative. Avoid wiping the emulsion " |
| "unless a lab confirms the defect is on the base side.\n" |
| "3. Inspect the scratch under 10x magnification. If physical, use " |
| "wet-gate or wet-mount scanning to reduce its appearance.\n" |
| "4. Anti-static treatment may be tested as a generic dust-control " |
| "step, but it should not be framed as HP5-specific.\n\n" |
| "## Confidence\n" |
| "Medium. The defect count is small, so the diagnosis should stay " |
| "conservative.\n\n" |
| "## Next inspection\n" |
| "Re-scan after cleaning. If dust changes position, scanner or " |
| "handling contamination is confirmed." |
| ), |
| }, |
| { |
| "role": "user", |
| "content": ( |
| "## Defect report\n" |
| "{\n" |
| ' "film_type": "Ilford HP5 (120 medium format)",\n' |
| ' "film_age_years": 15,\n' |
| ' "storage": "shoe box, attic",\n' |
| ' "metadata_confidence": "high",\n' |
| ' "defects": [\n' |
| ' {"label": "long_hair", "bbox_count": 4},\n' |
| ' {"label": "scratch", "bbox_count": 1}\n' |
| ' ],\n' |
| ' "scan_resolution_dpi": 3200\n' |
| "}\n\n" |
| "What is the root cause and what physical fixes do you recommend?" |
| ), |
| }, |
| { |
| "role": "assistant", |
| "content": ( |
| "## Root cause\n" |
| "Long hair and scratches on an aged, poorly stored roll indicate " |
| "physical abrasion from dust and debris inside the storage " |
| "container. Attic storage subjects film to temperature and " |
| "humidity swings that can make the emulsion more vulnerable.\n\n" |
| "## Evidence\n" |
| "- The storage field explicitly says attic, so heat and humidity " |
| "stress are supported for this case.\n" |
| "- Hair plus a scratch indicates physical contact, not only scanner " |
| "dust.\n\n" |
| "## Physical fixes\n" |
| "1. Inspect the negative under a 10x loupe to confirm hair is on " |
| "the emulsion side (matte) vs. base side (glossy). Emulsion-side " |
| "hair cannot be wet-wiped safely.\n" |
| "2. If base-side only: gently wipe with a PEC*PAD soaked in " |
| "Edwal Negative Cleaner, then PEC*PAD with distilled water, " |
| "then dry.\n" |
| "3. If emulsion-side: do not wipe. Use Print File archival " |
| "sleeves and ship to a lab with wet-mount scanning (e.g., " |
| "Northcoast Photographic).\n" |
| "4. The single scratch is most likely from a dirty film " |
| "transport. Have the scanner cleaned and the film path " |
| "inspected; consider wet-gate scanning to mask the scratch.\n" |
| "5. Long-term: transfer remaining unexposed rolls to a " |
| "freezer at -18C in sealed bags with desiccant.\n\n" |
| "## Confidence\n" |
| "Medium. The metadata supports storage stress, but the side of the " |
| "scratch and hair must be confirmed under magnification.\n\n" |
| "## Next inspection\n" |
| "Inspect both base and emulsion sides and note which defects stay " |
| "fixed after a gentle air blow." |
| ), |
| }, |
| { |
| "role": "user", |
| "content": ( |
| "## Defect report\n" |
| "{\n" |
| ' "film_type": "CineStill 800T (35mm)",\n' |
| ' "film_age_years": 0,\n' |
| ' "storage": "fresh, refrigerated by manufacturer",\n' |
| ' "metadata_confidence": "high",\n' |
| ' "defects": [\n' |
| ' {"label": "dust", "bbox_count": 156},\n' |
| ' {"label": "dirt", "bbox_count": 23},\n' |
| ' {"label": "short_hair", "bbox_count": 8}\n' |
| ' ],\n' |
| ' "scan_resolution_dpi": 6000\n' |
| "}\n\n" |
| "What is the root cause and what physical fixes do you recommend?" |
| ), |
| }, |
| { |
| "role": "assistant", |
| "content": ( |
| "## Root cause\n" |
| "Excessive dust and dirt on a brand-new refrigerated roll at 6000 " |
| "dpi scan suggests a combination of static during handling and " |
| "scanner-side contamination. The hair count points to the " |
| "scanner's film transport path. Because the current film type is " |
| "CineStill 800T, anti-static handling is especially worth testing " |
| "on a re-scan.\n\n" |
| "## Evidence\n" |
| "- The film type is CineStill 800T, so static-prone handling is " |
| "relevant for this case.\n" |
| "- The film is fresh and refrigerated, which makes chemical age " |
| "damage unlikely.\n" |
| "- Short hairs point to the holder or transport path.\n\n" |
| "## Physical fixes\n" |
| "1. Test anti-static treatment: discharge " |
| "the negative with a Zerostat gun on low setting 30 cm from the " |
| "film before scanning.\n" |
| "2. Clean the scanner glass, film holder, and feed rollers with " |
| "PEC*PAD swabs and reagent-grade isopropyl alcohol.\n" |
| "3. Use a static-discharge ionizing bar (e.g., Simco-Ion) at the " |
| "scanner input if available.\n" |
| "4. If the scanner and film workflow support hardware dust " |
| "removal, test it on one crop at moderate strength before a full " |
| "batch.\n" |
| "5. For the short hairs, inspect the film path under magnification " |
| "and remove any visible lint from the rollers with tweezers.\n\n" |
| "## Confidence\n" |
| "Medium-high because the metadata and defect mix are consistent.\n\n" |
| "## Next inspection\n" |
| "Re-scan after anti-static treatment. If the dust count drops, " |
| "static and handling were major contributors." |
| ), |
| }, |
| ] |
|
|
|
|
| def build_user_prompt( |
| film_type: str, |
| film_age_years: int, |
| storage: str, |
| scan_resolution_dpi: int, |
| defect_summary: dict[str, int], |
| total_defects: int, |
| spatial_evidence: dict[str, Any] | None = None, |
| metadata_confidence: str = "low", |
| ) -> str: |
| """Build the user message for the current diagnosis request.""" |
| payload = { |
| "defects": [ |
| {"label": label, "bbox_count": count} |
| for label, count in sorted(defect_summary.items()) |
| ], |
| "total_defect_count": total_defects, |
| "spatial_evidence": spatial_evidence or {}, |
| "film_type": film_type, |
| "film_age_years": film_age_years, |
| "storage": storage, |
| "scan_resolution_dpi": scan_resolution_dpi, |
| "metadata_confidence": metadata_confidence, |
| } |
| return ( |
| "## Defect report\n" |
| f"{json.dumps(payload, indent=2)}\n\n" |
| "Base the diagnosis on validated defects first. Use metadata only as " |
| "context according to metadata_confidence.\n\n" |
| "What is the root cause and what physical fixes do you recommend?" |
| ) |
|
|
|
|
| def build_messages( |
| film_type: str, |
| film_age_years: int, |
| storage: str, |
| scan_resolution_dpi: int, |
| defect_summary: dict[str, int], |
| total_defects: int, |
| spatial_evidence: dict[str, Any] | None = None, |
| metadata_confidence: str = "low", |
| ) -> list[dict[str, str]]: |
| """Return full message list for the reasoner. |
| |
| The list is structured for `tokenizer.apply_chat_template`: |
| [system, user, assistant, user, assistant, user, assistant, user] |
| The system prompt is index 0, the three few-shot examples follow as |
| (user, assistant) pairs, and the final user message is the actual |
| defect report. This preserves the role structure that |
| `Nemotron-Mini-4B-Instruct` was trained on. |
| """ |
| messages: list[dict[str, str]] = [ |
| {"role": "system", "content": SYSTEM_PROMPT}, |
| ] |
| messages.extend(FEW_SHOT_EXAMPLES) |
| messages.append( |
| { |
| "role": "user", |
| "content": build_user_prompt( |
| film_type, |
| film_age_years, |
| storage, |
| scan_resolution_dpi, |
| defect_summary, |
| total_defects, |
| spatial_evidence=spatial_evidence, |
| metadata_confidence=metadata_confidence, |
| ), |
| } |
| ) |
| return messages |
|
|