QLoRA adapters for hotel room cleanliness detection, fine-tuned on Qwen3-VL-4B-Instruct. Part of the roomaudit project.
Three adapters are included here, each from a different training approach. All were trained on the same synthetic dataset: 218 clean hotel room images with defects painted in using SAM3 + FLUX.1 Fill inpainting.
Single-turn format. Takes a room image, returns a JSON verdict with clean/messy classification and a defect list.
Two-turn format: Round 1 selects 1-2 regions to inspect, Round 2 gives the final verdict after seeing the crops. Scores below the single-turn adapter on the current synthetic dataset. Included as a reference for the agentic training approach.
Same single-turn format as the primary adapter, but with LoRA applied to the vision encoder as well as the language layers. Worse than LLM-only training: the ViT adapters learn to detect FLUX inpainting artefacts rather than actual room defects. Included as a reference.
1from huggingface_hub import snapshot_download
2from unsloth import FastVisionModel
3from peft import PeftModel
4from PIL import Image
5import json, re
6from qwen_vl_utils import process_vision_info
7
8snapshot_download(
9 "RanenSim/RoomAudit-Lora",
10 allow_patterns="lora_adapter/*",
11 local_dir="outputs/",
12)
13
14model, tokenizer = FastVisionModel.from_pretrained(
15 "unsloth/Qwen3-VL-4B-Instruct-unsloth-bnb-4bit",
16 load_in_4bit=True,
17)
18model = PeftModel.from_pretrained(model, "outputs/lora_adapter")
19FastVisionModel.for_inference(model)
20
21image = Image.open("room.jpg").convert("RGB")
22messages = [
23 {"role": "system", "content": [{"type": "text", "text": "You are a hotel room cleanliness inspector. Respond ONLY with valid JSON."}]},
24 {"role": "user", "content": [
25 {"type": "image", "image": image},
26 {"type": "text", "text": '{"clean": true/false, "defects": [{"object": "...", "type": "...", "description": "..."}]}'},
27 ]},
28]
29text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
30image_inputs, _ = process_vision_info(messages)
31inputs = tokenizer(text=[text], images=image_inputs, padding=True, return_tensors="pt").to("cuda")
32out_ids = model.generate(**inputs, max_new_tokens=256, do_sample=True, temperature=0.1)
33output = tokenizer.decode(out_ids[0][inputs.input_ids.shape[1]:], skip_special_tokens=True)
34result = json.loads(re.search(r"\{.*\}", output, re.DOTALL).group())
See each adapter's README for full usage instructions, training config, and results.
Source code, training notebooks, and data generation pipeline:
github.com/Razorbird360/roomaudit