fix(caption): remove dead min_length param, split Florence/PromptGen prompts, fix gaze detection

- Remove caption_openclip_min_length from settings, API models, endpoints, and UI
  (clip_interrogator library has no min_length support; parameter was never functional)
- Split vlm_prompts_florence into base Florence prompts and PromptGen-only prompts
  (GENERATE_TAGS, Analyze, Mixed Caption require MiaoshouAI PromptGen fine-tune)
- Add 'promptgen' category to /vqa/prompts API endpoint
- Fix gaze detection: move DETECT_GAZE check before generic 'detect ' prefix
  to prevent "Detect Gaze" matching as detect target="Gaze"
- Update test suite: remove min_length tests, fix min_flavors to use mode='best',
  add acceptance-only notes, fix thinking trace detection, improve bracket/OCR tests,
  split Florence/PromptGen test coverage
This commit is contained in:
CalamitousFelicitousness
2026-01-29 01:21:28 +00:00
parent fba942b25e
commit bf7a72f12e
6 changed files with 214 additions and 124 deletions
+3 -5
View File
@@ -145,8 +145,6 @@ def post_caption(req: models.ReqCaption):
raise HTTPException(status_code=404, detail="Model not found")
# Build clip overrides from request (only include non-None values)
clip_overrides = {}
if req.min_length is not None:
clip_overrides['min_length'] = req.min_length
if req.max_length is not None:
clip_overrides['max_length'] = req.max_length
if req.chunk_size is not None:
@@ -314,8 +312,6 @@ def _dispatch_openclip(req: models.ReqCaptionOpenCLIP) -> models.ResCaptionDispa
raise HTTPException(status_code=404, detail="Model not found")
# Build clip overrides from request
clip_overrides = {}
if req.min_length is not None:
clip_overrides['min_length'] = req.min_length
if req.max_length is not None:
clip_overrides['max_length'] = req.max_length
if req.chunk_size is not None:
@@ -499,7 +495,8 @@ def get_vqa_prompts(model: Optional[str] = None):
**Prompt Categories:**
- Common: Use Prompt, Short/Normal/Long Caption
- Florence: Phrase Grounding, Object Detection, OCR, Dense Region Caption
- Florence: Phrase Grounding, Object Detection, OCR, Dense Region Caption (all Florence models)
- PromptGen: Analyze, Generate Tags, Mixed Caption (MiaoshouAI PromptGen fine-tunes only)
- Moondream: Point at..., Detect all..., Detect Gaze
"""
from modules.caption import vqa
@@ -509,6 +506,7 @@ def get_vqa_prompts(model: Optional[str] = None):
return {
"common": vqa.vlm_prompts_common,
"florence": vqa.vlm_prompts_florence,
"promptgen": vqa.vlm_prompts_promptgen,
"moondream": vqa.vlm_prompts_moondream,
"moondream2_only": vqa.vlm_prompts_moondream2
}
-2
View File
@@ -378,7 +378,6 @@ class ReqCaption(BaseModel):
mode: str = Field(default="best", title="Mode", description="Caption mode. Fast: Quick caption with minimal flavor terms. Classic: Standard captioning with balanced quality and speed. Best: Most thorough analysis, slowest but highest quality. Negative: Generate terms to use as negative prompt.")
analyze: bool = Field(default=False, title="Analyze", description="If True, returns detailed image analysis breakdown (medium, artist, movement, trending, flavor) in addition to caption.")
# Advanced settings (optional per-request overrides)
min_length: Optional[int] = Field(default=None, title="Min Length", description="Minimum number of tokens in the generated caption.")
max_length: Optional[int] = Field(default=None, title="Max Length", description="Maximum number of tokens in the generated caption.")
chunk_size: Optional[int] = Field(default=None, title="Chunk Size", description="Batch size for processing description candidates (flavors). Higher values speed up captioning but increase VRAM usage.")
min_flavors: Optional[int] = Field(default=None, title="Min Flavors", description="Minimum number of descriptive tags (flavors) to keep in the final prompt.")
@@ -486,7 +485,6 @@ class ReqCaptionOpenCLIP(BaseModel):
blip_model: str = Field(default="blip-large", title="Caption Model", description="BLIP model used to generate the initial image caption.")
mode: str = Field(default="best", title="Mode", description="Caption mode: 'best' (highest quality), 'fast' (quick), 'classic' (traditional), 'caption' (BLIP only), 'negative' (for negative prompts).")
analyze: bool = Field(default=False, title="Analyze", description="If True, returns detailed breakdown (medium, artist, movement, trending, flavor).")
min_length: Optional[int] = Field(default=None, title="Min Length", description="Minimum tokens in generated caption.")
max_length: Optional[int] = Field(default=None, title="Max Length", description="Maximum tokens in generated caption.")
chunk_size: Optional[int] = Field(default=None, title="Chunk Size", description="Batch size for processing flavors.")
min_flavors: Optional[int] = Field(default=None, title="Min Flavors", description="Minimum descriptive tags to keep.")
+36 -23
View File
@@ -80,7 +80,7 @@ vlm_prompts_common = [
"Long Caption",
]
# Florence-2 specific prompts (only shown for Florence/PromptGen models)
# Florence-2 base prompts (supported by all Florence models including CogFlorence)
vlm_prompts_florence = [
"Phrase Grounding",
"Object Detection",
@@ -88,6 +88,10 @@ vlm_prompts_florence = [
"Region Proposal",
"OCR (Read Text)",
"OCR with Regions",
]
# PromptGen-only prompts (require MiaoshouAI PromptGen fine-tune)
vlm_prompts_promptgen = [
"Analyze",
"Generate Tags",
"Mixed Caption",
@@ -148,7 +152,7 @@ vlm_prompt_placeholders = {
}
# Legacy list for backwards compatibility
vlm_prompts = vlm_prompts_common + vlm_prompts_florence + vlm_prompts_moondream + vlm_prompts_moondream2
vlm_prompts = vlm_prompts_common + vlm_prompts_florence + vlm_prompts_promptgen + vlm_prompts_moondream + vlm_prompts_moondream2
vlm_prefill = 'Answer: the image shows'
@@ -160,8 +164,12 @@ def get_prompts_for_model(model_name: str) -> list:
model_lower = model_name.lower()
# Check for Florence-2 / PromptGen models
if 'florence' in model_lower or 'promptgen' in model_lower:
# Check for PromptGen models (MiaoshouAI fine-tunes with extra prompts)
if 'promptgen' in model_lower:
return vlm_prompts_common + vlm_prompts_florence + vlm_prompts_promptgen
# Check for Florence-2 base / CogFlorence models (no PromptGen-specific prompts)
if 'florence' in model_lower:
return vlm_prompts_common + vlm_prompts_florence
# Check for Moondream models (Moondream 2 has gaze detection, Moondream 3 does not)
@@ -194,17 +202,21 @@ def get_prompt_placeholder(friendly_name: str) -> str:
def is_florence_task(question: str) -> bool:
"""Check if the question is a Florence-2 task token (either friendly name or internal token)."""
"""Check if the question is a Florence-2 task token (either friendly name or internal token).
This includes both base Florence prompts and PromptGen-specific prompts,
since all are handled by the Florence handler.
"""
if not question:
return False
# Check if it's a Florence-specific friendly name
if question in vlm_prompts_florence:
# Check if it's a Florence-specific friendly name (base or PromptGen)
if question in vlm_prompts_florence or question in vlm_prompts_promptgen:
return True
# Check if it's an internal Florence-2 task token (for backwards compatibility)
florence_tokens = ['<CAPTION>', '<DETAILED_CAPTION>', '<MORE_DETAILED_CAPTION>', '<CAPTION_TO_PHRASE_GROUNDING>',
'<OD>', '<DENSE_REGION_CAPTION>', '<REGION_PROPOSAL>', '<OCR>', '<OCR_WITH_REGION>',
'<ANALYZE>', '<GENERATE_TAGS>', '<MIXED_CAPTION>', '<MIXED_CAPTION_PLUS>']
return question in florence_tokens
florence_base_tokens = ['<CAPTION>', '<DETAILED_CAPTION>', '<MORE_DETAILED_CAPTION>', '<CAPTION_TO_PHRASE_GROUNDING>',
'<OD>', '<DENSE_REGION_CAPTION>', '<REGION_PROPOSAL>', '<OCR>', '<OCR_WITH_REGION>']
promptgen_tokens = ['<ANALYZE>', '<GENERATE_TAGS>', '<MIXED_CAPTION>', '<MIXED_CAPTION_PLUS>']
return question in florence_base_tokens or question in promptgen_tokens
def is_thinking_model(model_name: str) -> bool:
@@ -1125,19 +1137,8 @@ class VQA:
self.last_detection_data = {'points': points}
return vqa_detection.format_points_text(points)
return "Object not found"
elif question.lower().startswith('detect ') or question == 'DETECT_MODE':
target = question[7:].strip() if question.lower().startswith('detect ') else ''
if not target:
return "Please specify an object to detect"
debug(f'VQA caption: handler=moondream method=detect target="{target}"')
result = self.model.detect(image, target)
debug(f'VQA caption: handler=moondream detect_raw_result={result}')
detections = vqa_detection.parse_detections(result, target)
if detections:
self.last_detection_data = {'detections': detections}
return vqa_detection.format_detections_text(detections, include_confidence=False)
return "No objects detected"
elif question == 'DETECT_GAZE' or question.lower() == 'detect gaze':
# Must be checked before generic 'detect ' prefix to avoid matching as detect target="Gaze"
debug('VQA caption: handler=moondream method=detect_gaze')
faces = self.model.detect(image, "face")
debug(f'VQA caption: handler=moondream detect_gaze faces={faces}')
@@ -1150,6 +1151,18 @@ class VQA:
self.last_detection_data = {'points': [(gaze['x'], gaze['y'])]}
return f"Gaze direction: ({gaze['x']:.3f}, {gaze['y']:.3f})"
return "No face/gaze detected"
elif question.lower().startswith('detect ') or question == 'DETECT_MODE':
target = question[7:].strip() if question.lower().startswith('detect ') else ''
if not target:
return "Please specify an object to detect"
debug(f'VQA caption: handler=moondream method=detect target="{target}"')
result = self.model.detect(image, target)
debug(f'VQA caption: handler=moondream detect_raw_result={result}')
detections = vqa_detection.parse_detections(result, target)
if detections:
self.last_detection_data = {'detections': detections}
return vqa_detection.format_detections_text(detections, include_confidence=False)
return "No objects detected"
else:
debug(f'VQA caption: handler=moondream method=query question="{question}" reasoning={thinking_mode}')
result = self.model.query(image, question, reasoning=thinking_mode)
-1
View File
@@ -749,7 +749,6 @@ options_templates.update(options_section(('hidden_options', "Hidden options"), {
"caption_openclip_mode": OptionInfo(caption_types[0], "OpenCLiP: default mode", gr.Dropdown, {"choices": caption_types, "visible": False}),
"caption_openclip_blip_model": OptionInfo(list(caption_models)[0], "OpenCLiP: default captioner", gr.Dropdown, {"choices": list(caption_models), "visible": False}),
"caption_openclip_num_beams": OptionInfo(1, "OpenCLiP: num beams", gr.Slider, {"minimum": 1, "maximum": 16, "step": 1, "visible": False}),
"caption_openclip_min_length": OptionInfo(32, "OpenCLiP: min length", gr.Slider, {"minimum": 1, "maximum": 128, "step": 1, "visible": False}),
"caption_openclip_max_length": OptionInfo(74, "OpenCLiP: max length", gr.Slider, {"minimum": 1, "maximum": 512, "step": 1, "visible": False}),
"caption_openclip_min_flavors": OptionInfo(2, "OpenCLiP: min flavors", gr.Slider, {"minimum": 0, "maximum": 32, "step": 1, "visible": False}),
"caption_openclip_max_flavors": OptionInfo(16, "OpenCLiP: max flavors", gr.Slider, {"minimum": 0, "maximum": 32, "step": 1, "visible": False}),
+7 -10
View File
@@ -112,8 +112,7 @@ def update_tagger_params(model_name, general_threshold, character_threshold, inc
def update_clip_params(*args):
clip_min_length, clip_max_length, clip_chunk_size, clip_min_flavors, clip_max_flavors, clip_flavor_count, clip_num_beams = args
shared.opts.caption_openclip_min_length = int(clip_min_length)
clip_max_length, clip_chunk_size, clip_min_flavors, clip_max_flavors, clip_flavor_count, clip_num_beams = args
shared.opts.caption_openclip_max_length = int(clip_max_length)
shared.opts.caption_openclip_min_flavors = int(clip_min_flavors)
shared.opts.caption_openclip_max_flavors = int(clip_max_flavors)
@@ -215,7 +214,6 @@ def create_ui():
clip_mode = gr.Dropdown(openclip.caption_types, label='Mode', value='fast', elem_id='clip_clip_mode')
with gr.Accordion(label='Caption: Advanced Options', open=False, visible=True):
with gr.Row():
clip_min_length = gr.Slider(label='clip: min length', value=shared.opts.caption_openclip_min_length, minimum=8, maximum=75, step=1, elem_id='clip_caption_min_length')
clip_max_length = gr.Slider(label='clip: max length', value=shared.opts.caption_openclip_max_length, minimum=16, maximum=1024, step=1, elem_id='clip_caption_max_length')
clip_chunk_size = gr.Slider(label='clip: chunk size', value=shared.opts.caption_openclip_chunk_size, minimum=256, maximum=4096, step=8, elem_id='clip_chunk_size')
with gr.Row():
@@ -224,13 +222,12 @@ def create_ui():
clip_flavor_count = gr.Slider(label='clip: intermediates', value=shared.opts.caption_openclip_flavor_count, minimum=256, maximum=4096, step=8, elem_id='clip_flavor_intermediate_count')
with gr.Row():
clip_num_beams = gr.Slider(label='clip: num beams', value=shared.opts.caption_openclip_num_beams, minimum=1, maximum=16, step=1, elem_id='clip_num_beams')
clip_min_length.change(fn=update_clip_params, inputs=[clip_min_length, clip_max_length, clip_chunk_size, clip_min_flavors, clip_max_flavors, clip_flavor_count, clip_num_beams], outputs=[])
clip_max_length.change(fn=update_clip_params, inputs=[clip_min_length, clip_max_length, clip_chunk_size, clip_min_flavors, clip_max_flavors, clip_flavor_count, clip_num_beams], outputs=[])
clip_chunk_size.change(fn=update_clip_params, inputs=[clip_min_length, clip_max_length, clip_chunk_size, clip_min_flavors, clip_max_flavors, clip_flavor_count, clip_num_beams], outputs=[])
clip_min_flavors.change(fn=update_clip_params, inputs=[clip_min_length, clip_max_length, clip_chunk_size, clip_min_flavors, clip_max_flavors, clip_flavor_count, clip_num_beams], outputs=[])
clip_max_flavors.change(fn=update_clip_params, inputs=[clip_min_length, clip_max_length, clip_chunk_size, clip_min_flavors, clip_max_flavors, clip_flavor_count, clip_num_beams], outputs=[])
clip_flavor_count.change(fn=update_clip_params, inputs=[clip_min_length, clip_max_length, clip_chunk_size, clip_min_flavors, clip_max_flavors, clip_flavor_count, clip_num_beams], outputs=[])
clip_num_beams.change(fn=update_clip_params, inputs=[clip_min_length, clip_max_length, clip_chunk_size, clip_min_flavors, clip_max_flavors, clip_flavor_count, clip_num_beams], outputs=[])
clip_max_length.change(fn=update_clip_params, inputs=[clip_max_length, clip_chunk_size, clip_min_flavors, clip_max_flavors, clip_flavor_count, clip_num_beams], outputs=[])
clip_chunk_size.change(fn=update_clip_params, inputs=[clip_max_length, clip_chunk_size, clip_min_flavors, clip_max_flavors, clip_flavor_count, clip_num_beams], outputs=[])
clip_min_flavors.change(fn=update_clip_params, inputs=[clip_max_length, clip_chunk_size, clip_min_flavors, clip_max_flavors, clip_flavor_count, clip_num_beams], outputs=[])
clip_max_flavors.change(fn=update_clip_params, inputs=[clip_max_length, clip_chunk_size, clip_min_flavors, clip_max_flavors, clip_flavor_count, clip_num_beams], outputs=[])
clip_flavor_count.change(fn=update_clip_params, inputs=[clip_max_length, clip_chunk_size, clip_min_flavors, clip_max_flavors, clip_flavor_count, clip_num_beams], outputs=[])
clip_num_beams.change(fn=update_clip_params, inputs=[clip_max_length, clip_chunk_size, clip_min_flavors, clip_max_flavors, clip_flavor_count, clip_num_beams], outputs=[])
with gr.Accordion(label='Caption: Batch', open=False, visible=True):
with gr.Row():
clip_batch_files = gr.File(label="Files", show_label=True, file_count='multiple', file_types=['image'], interactive=True, height=100, elem_id='clip_batch_files')