Download editscore/json_parser.py from chibifire/EditScore-code: direct link, hf CLI and curl.
- Browser
- Download file 7.16 kB
-
https://huggingface.co/chibifire/EditScore-code/resolve/main/editscore/json_parser.py
- Command line
-
hf download hf://chibifire/EditScore-code/editscore/json_parser.py
-
curl -L -o json_parser.py https://huggingface.co/chibifire/EditScore-code/resolve/main/editscore/json_parser.py
7.16 kB
| import json | |
| import re | |
| from typing import Dict, Any, List, Optional | |
| # ============================================================================== | |
| # HELPER FUNCTIONS (Based on your provided robust fixers) | |
| # ============================================================================== | |
| # For clarity, these are named as internal functions (prefixed with an underscore). | |
| def _fix_json_quotes(s: str) -> str: | |
| """First-stage repair: handle incorrect quotes and basic structure.""" | |
| # Replace Python-style booleans/None with JSON standard | |
| s = re.sub(r'\bTrue\b', 'true', s) | |
| s = re.sub(r'\bFalse\b', 'false', s) | |
| s = re.sub(r'\bNone\b', 'null', s) | |
| # Attempt to replace single quotes with double quotes (a common VLM error) | |
| # This is a high-risk operation that might break the reasoning content, | |
| # but it's worth trying early on. | |
| try: | |
| temp_s = s.replace("'", '"') | |
| json.loads(temp_s) | |
| return temp_s | |
| except json.JSONDecodeError: | |
| # If it's still invalid after replacement, return the original string for the next repair step. | |
| pass | |
| # Add double quotes to keys (e.g., {reasoning: ...} -> {"reasoning": ...}) | |
| s = re.sub(r'([\{\s,])(\w+)\s*:', r'\1"\2":', s) | |
| return s | |
| def _repair_reasoning_field_robust(json_str: str) -> str: | |
| """Second-stage repair: specifically fix unescaped double quotes inside the 'reasoning' field.""" | |
| pattern = re.compile( | |
| r'("reasoning"\s*:\s*")' # --- Group 1: "reasoning" : " | |
| r'(.*?)' # --- Group 2: The content (non-greedy) | |
| r'(?="\s*[,}])', # --- Lookahead: find " followed by , or } | |
| re.DOTALL | |
| ) | |
| def replacer(match): | |
| prefix = match.group(1) | |
| content = match.group(2) | |
| # In the content, replace all unescaped " with \" | |
| fixed_content = content.replace('"', '\\"') | |
| return prefix + fixed_content | |
| return pattern.sub(replacer, json_str) | |
| def _fallback_extract_and_rebuild(input_str: str) -> str: | |
| """Final fallback strategy: abandon repair, directly extract information, and rebuild a valid JSON.""" | |
| # 1. Extract reasoning | |
| # Find all content between "reasoning": and ,"score": | |
| reasoning_text = "" | |
| reason_match = re.search(r'["\']reasoning["\']\s*:\s*["\']?(.*?)["\']?\s*,\s*["\']score["\']', input_str, re.DOTALL | re.IGNORECASE) | |
| if reason_match: | |
| reasoning_text = reason_match.group(1).strip() | |
| # Clean up any potentially remaining escape characters | |
| reasoning_text = reasoning_text.replace('\\"', '"') | |
| else: | |
| # If not found, assume all text besides the score part is the reasoning. | |
| # First, remove the score part. | |
| score_part_match = re.search(r'["\']score["\']\s*:.*', input_str, re.IGNORECASE) | |
| if score_part_match: | |
| reasoning_text = input_str[:score_part_match.start()].strip() | |
| else: | |
| # If even 'score' cannot be found, assume the entire string is the reasoning. | |
| reasoning_text = input_str | |
| # 2. Extract scores | |
| scores = [] | |
| # Prioritize searching after the 'score' keyword. | |
| score_match = re.search(r'["\']score["\']\s*:\s*(.*)', input_str, re.DOTALL | re.IGNORECASE) | |
| search_area = score_match.group(1) if score_match else input_str | |
| # Find all integers or floats. | |
| numbers = re.findall(r'[-+]?\d*\.?\d+', search_area) | |
| if numbers: | |
| scores = [float(num) for num in numbers] | |
| # 3. Rebuild into a standard dictionary and return a JSON string. | |
| rebuilt_data = { | |
| "reasoning": reasoning_text, | |
| "score": scores | |
| } | |
| return json.dumps(rebuilt_data, ensure_ascii=False) | |
| def _format_and_validate_dict(data: Dict[str, Any]) -> Optional[Dict[str, Any]]: | |
| """Validate and format the parsed dictionary to ensure it meets the final output standard.""" | |
| if not isinstance(data, dict): | |
| return None | |
| # Extract reasoning, tolerating case and spelling variations. | |
| reasoning = "" | |
| for key in ["reasoning", "reason", "rationale"]: | |
| if key in data and isinstance(data[key], str): | |
| reasoning = data[key] | |
| break | |
| # Extract score, and ensure it is a list of floats. | |
| scores = [] | |
| if 'score' in data: | |
| score_val = data['score'] | |
| if isinstance(score_val, list): | |
| scores = [float(s) for s in score_val if isinstance(s, (int, float, str))] | |
| elif isinstance(score_val, (int, float)): | |
| scores = [float(score_val)] | |
| # If any field was found, consider it a success. | |
| if reasoning or scores: | |
| return {"score": scores, "reasoning": reasoning} | |
| return None | |
| # ============================================================================== | |
| # MAIN PARSING FUNCTION | |
| # ============================================================================== | |
| def parse_vlm_output_to_dict(input_string: str) -> Dict[str, Any]: | |
| """ | |
| A highly robust function to parse a VLM's output string into a dictionary | |
| containing 'score' and 'reasoning'. | |
| It uses a multi-stage repair pipeline, progressively degrading from standard | |
| JSON parsing to a final information extraction fallback. | |
| """ | |
| # --- 0. Preprocessing --- | |
| if not input_string or not input_string.strip(): | |
| return {"score": [], "reasoning": "Input was empty."} | |
| # Find the substring enclosed by `{}`, which is often the core of the VLM output. | |
| json_match = re.search(r'\{.*\}', input_string, re.DOTALL) | |
| target_str = json_match.group(0) if json_match else input_string.strip() | |
| # --- Repair Pipeline --- | |
| # Apply fixers in order, attempting to parse after each one. | |
| fixer_pipeline = [ | |
| lambda s: s, # 1. Try the original string. | |
| _fix_json_quotes, # 2. Fix basic quotes and keywords. | |
| _repair_reasoning_field_robust, # 3. Fix internal quotes in the reasoning field. | |
| ] | |
| for fixer in fixer_pipeline: | |
| try: | |
| fixed_str = fixer(target_str) | |
| data = json.loads(fixed_str) | |
| validated_data = _format_and_validate_dict(data) | |
| if validated_data is not None: | |
| return validated_data | |
| except (json.JSONDecodeError, TypeError): | |
| # If it fails, continue to the next fixer. | |
| continue | |
| # --- Final Fallback Strategy --- | |
| # If all repair and parsing attempts fail, activate the information extraction mode. | |
| try: | |
| fallback_str = _fallback_extract_and_rebuild(target_str) | |
| # This function guarantees a valid JSON string, so we can load it directly. | |
| data = json.loads(fallback_str) | |
| # Still run it through the validator to standardize the format. | |
| validated_data = _format_and_validate_dict(data) | |
| if validated_data: | |
| return validated_data | |
| except Exception: | |
| # If even the final fallback strategy fails, return an error message. | |
| pass | |
| return { | |
| "score": [], | |
| "reasoning": f"Failed to parse after all strategies. Original output: '{input_string}'" | |
| } | |