BhargavMN commited on
Commit Β·
30aba87
1
Parent(s): f7f184b
Hide and seek dataset for fine tuninig
Browse files
.gitignore
CHANGED
|
@@ -110,3 +110,30 @@ app/data/*/run_generation.log
|
|
| 110 |
# other files to ignore
|
| 111 |
CityQuest_AI_Project_Specification.md
|
| 112 |
ai_pipeline_source_of_truth.md
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
# other files to ignore
|
| 111 |
CityQuest_AI_Project_Specification.md
|
| 112 |
ai_pipeline_source_of_truth.md
|
| 113 |
+
scavenger_hunt_pipeline_summary.md
|
| 114 |
+
keys.txt
|
| 115 |
+
scripts/.key_state
|
| 116 |
+
scripts/key_manager.py
|
| 117 |
+
scripts/scavenger_hunt/run_generation.py
|
| 118 |
+
prompts/hide_and_seek_spec.md
|
| 119 |
+
prompts/tag_spec.md
|
| 120 |
+
scripts/common/cq_common.py
|
| 121 |
+
scripts/hide_and_seek/generator.py
|
| 122 |
+
scripts/hide_and_seek/run_generation.py
|
| 123 |
+
scripts/hide_and_seek/sampler.py
|
| 124 |
+
scripts/hide_and_seek/test_pipeline.py
|
| 125 |
+
scripts/hide_and_seek/validate_dataset.py
|
| 126 |
+
scripts/hide_and_seek/validator.py
|
| 127 |
+
scripts/scavenger_hunt/generator.py
|
| 128 |
+
|
| 129 |
+
scripts/scavenger_hunt/run_generation.py
|
| 130 |
+
scripts/scavenger_hunt/sampler.py
|
| 131 |
+
scripts/scavenger_hunt/validator.py
|
| 132 |
+
|
| 133 |
+
scripts/tag/sampler.py
|
| 134 |
+
scripts/scavenger_hunt/validator.py
|
| 135 |
+
scripts/tag/generator.py
|
| 136 |
+
scripts/tag/run_generation.py
|
| 137 |
+
scripts/tag/test_pipeline.py
|
| 138 |
+
scripts/tag/validate_dataset.py
|
| 139 |
+
scripts/tag/validator.py
|
app/data/hide_and_seek/dataset.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
scripts/scavenger_hunt/generator.py
CHANGED
|
@@ -246,6 +246,7 @@ def main():
|
|
| 246 |
errors_path = args.output.replace(".json", "_errors.jsonl")
|
| 247 |
start_idx = len(dataset) + 1
|
| 248 |
kept = skipped = 0
|
|
|
|
| 249 |
|
| 250 |
# ββ generation loop βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 251 |
for i in range(start_idx, start_idx + args.n):
|
|
@@ -274,6 +275,11 @@ def main():
|
|
| 274 |
f.write(json.dumps({"id": record["id"], "errors": errors}) + "\n")
|
| 275 |
log.info(" β skipped after retries β logged to %s", errors_path)
|
| 276 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 277 |
if not args.mock:
|
| 278 |
time.sleep(args.sleep)
|
| 279 |
|
|
@@ -286,6 +292,9 @@ def main():
|
|
| 286 |
if skipped:
|
| 287 |
log.info(" Failures: %s", errors_path)
|
| 288 |
|
|
|
|
|
|
|
|
|
|
| 289 |
|
| 290 |
if __name__ == "__main__":
|
| 291 |
main()
|
|
|
|
| 246 |
errors_path = args.output.replace(".json", "_errors.jsonl")
|
| 247 |
start_idx = len(dataset) + 1
|
| 248 |
kept = skipped = 0
|
| 249 |
+
quota_exhausted = False
|
| 250 |
|
| 251 |
# ββ generation loop βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 252 |
for i in range(start_idx, start_idx + args.n):
|
|
|
|
| 275 |
f.write(json.dumps({"id": record["id"], "errors": errors}) + "\n")
|
| 276 |
log.info(" β skipped after retries β logged to %s", errors_path)
|
| 277 |
|
| 278 |
+
if not args.mock and any("RESOURCE_EXHAUSTED" in e or "429" in e for e in errors):
|
| 279 |
+
log.warning(" quota exhausted on this API key β stopping run early")
|
| 280 |
+
quota_exhausted = True
|
| 281 |
+
break
|
| 282 |
+
|
| 283 |
if not args.mock:
|
| 284 |
time.sleep(args.sleep)
|
| 285 |
|
|
|
|
| 292 |
if skipped:
|
| 293 |
log.info(" Failures: %s", errors_path)
|
| 294 |
|
| 295 |
+
if quota_exhausted:
|
| 296 |
+
raise SystemExit(2)
|
| 297 |
+
|
| 298 |
|
| 299 |
if __name__ == "__main__":
|
| 300 |
main()
|
scripts/scavenger_hunt/run_generation.py
CHANGED
|
@@ -20,6 +20,9 @@ import sys
|
|
| 20 |
from collections import Counter
|
| 21 |
|
| 22 |
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
|
|
|
|
|
|
| 23 |
GENERATOR = os.path.join(HERE, "generator.py")
|
| 24 |
DEFAULT_OUTPUT = os.path.normpath(os.path.join(
|
| 25 |
HERE, "..", "..", "app", "data", "scavenger_hunt", "dataset.json"))
|
|
@@ -55,8 +58,8 @@ def main():
|
|
| 55 |
ap.add_argument("--sleep", type=float, default=8.0, help="seconds between Gemini calls (passed to generator.py)")
|
| 56 |
args = ap.parse_args()
|
| 57 |
|
| 58 |
-
if not
|
| 59 |
-
print("
|
| 60 |
sys.exit(1)
|
| 61 |
|
| 62 |
try:
|
|
@@ -67,11 +70,27 @@ def main():
|
|
| 67 |
print(f"\nTarget reached: {current}/{args.target} examples in {args.output}")
|
| 68 |
break
|
| 69 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
n = min(args.batch, args.target - current)
|
| 71 |
-
print(f"\n=== Generating {n} example(s) ({current} -> {current + n} of {args.target}) ===")
|
|
|
|
|
|
|
|
|
|
| 72 |
result = subprocess.run(
|
| 73 |
-
[sys.executable, GENERATOR, "--n", str(n), "--output", args.output, "--sleep", str(args.sleep)]
|
|
|
|
| 74 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
if result.returncode != 0:
|
| 76 |
print("generator.py exited with an error β stopping.")
|
| 77 |
break
|
|
|
|
| 20 |
from collections import Counter
|
| 21 |
|
| 22 |
HERE = os.path.dirname(os.path.abspath(__file__))
|
| 23 |
+
sys.path.insert(0, os.path.join(HERE, ".."))
|
| 24 |
+
import key_manager
|
| 25 |
+
|
| 26 |
GENERATOR = os.path.join(HERE, "generator.py")
|
| 27 |
DEFAULT_OUTPUT = os.path.normpath(os.path.join(
|
| 28 |
HERE, "..", "..", "app", "data", "scavenger_hunt", "dataset.json"))
|
|
|
|
| 58 |
ap.add_argument("--sleep", type=float, default=8.0, help="seconds between Gemini calls (passed to generator.py)")
|
| 59 |
args = ap.parse_args()
|
| 60 |
|
| 61 |
+
if not key_manager.load_keys():
|
| 62 |
+
print(f"No API keys found in {key_manager.KEYS_FILE}. Add at least one key (one per line) before running.")
|
| 63 |
sys.exit(1)
|
| 64 |
|
| 65 |
try:
|
|
|
|
| 70 |
print(f"\nTarget reached: {current}/{args.target} examples in {args.output}")
|
| 71 |
break
|
| 72 |
|
| 73 |
+
idx, key = key_manager.get_active_key()
|
| 74 |
+
if key is None:
|
| 75 |
+
print(f"\nAll API keys in {key_manager.KEYS_FILE} are exhausted.")
|
| 76 |
+
print(f"Progress so far: {current}/{args.target} examples in {args.output}")
|
| 77 |
+
print("Add more keys or wait for quotas to reset, then re-run to resume.")
|
| 78 |
+
break
|
| 79 |
+
|
| 80 |
n = min(args.batch, args.target - current)
|
| 81 |
+
print(f"\n=== Generating {n} example(s) ({current} -> {current + n} of {args.target}) [key #{idx + 1}] ===")
|
| 82 |
+
|
| 83 |
+
env = os.environ.copy()
|
| 84 |
+
env["GEMINI_API_KEY"] = key
|
| 85 |
result = subprocess.run(
|
| 86 |
+
[sys.executable, GENERATOR, "--n", str(n), "--output", args.output, "--sleep", str(args.sleep)],
|
| 87 |
+
env=env,
|
| 88 |
)
|
| 89 |
+
|
| 90 |
+
if result.returncode == 2:
|
| 91 |
+
print(f" Key #{idx + 1} quota exhausted β rotating to the next key.")
|
| 92 |
+
key_manager.rotate_key()
|
| 93 |
+
continue
|
| 94 |
if result.returncode != 0:
|
| 95 |
print("generator.py exited with an error β stopping.")
|
| 96 |
break
|
scripts/scavenger_hunt/sampler.py
CHANGED
|
@@ -4,42 +4,13 @@ Samples ONE input config in Python (distributions guaranteed here, not by the LL
|
|
| 4 |
and builds a prompt asking Gemini to write only the output for that input.
|
| 5 |
"""
|
| 6 |
import json
|
|
|
|
| 7 |
import random
|
|
|
|
| 8 |
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
CITY_BANK
|
| 12 |
-
"Paris": ("France", "PAR", ["iconic_landmark","wide_boulevard","river_waterfront","garden_formal","historic_district","cafe_dense","museum_cluster"], "temperate"),
|
| 13 |
-
"Tokyo": ("Japan", "TYO", ["dense_urban_grid","narrow_alley_network","religious_site_accessible","market_covered","iconic_landmark","shopping_street"], "temperate"),
|
| 14 |
-
"New York City": ("USA", "NYC", ["dense_urban_grid","park_large","iconic_landmark","coastal_waterfront","museum_cluster","wide_boulevard"], "continental"),
|
| 15 |
-
"Cape Town": ("South Africa", "CPT", ["coastal_waterfront","hill_or_elevation","market_outdoor","historic_district","garden_formal"], "mediterranean"),
|
| 16 |
-
"Marrakech": ("Morocco", "MRK", ["narrow_alley_network","market_outdoor","religious_site_accessible","plaza_or_square","food_market"], "arid"),
|
| 17 |
-
"Buenos Aires": ("Argentina", "BUE", ["wide_boulevard","plaza_or_square","cafe_dense","street_art_district","historic_district"], "temperate"),
|
| 18 |
-
"Mumbai": ("India", "BOM", ["coastal_waterfront","market_outdoor","dense_urban_grid","religious_site_accessible","food_market"], "tropical"),
|
| 19 |
-
"Berlin": ("Germany", "BER", ["park_large","street_art_district","museum_cluster","historic_district","wide_boulevard","cafe_dense"], "continental"),
|
| 20 |
-
"Sydney": ("Australia", "SYD", ["coastal_waterfront","park_large","iconic_landmark","market_covered","bridge_pedestrian"], "temperate"),
|
| 21 |
-
"Nairobi": ("Kenya", "NBO", ["park_large","market_outdoor","residential_neighbourhood","dense_urban_grid"], "tropical"),
|
| 22 |
-
"Istanbul": ("Turkey", "IST", ["historic_district","religious_site_accessible","market_covered","coastal_waterfront","hill_or_elevation","plaza_or_square"], "mediterranean"),
|
| 23 |
-
"Mexico City": ("Mexico", "MEX", ["plaza_or_square","market_outdoor","museum_cluster","street_art_district","dense_urban_grid"], "temperate"),
|
| 24 |
-
"Amsterdam": ("Netherlands", "AMS", ["river_waterfront","bridge_pedestrian","narrow_alley_network","cafe_dense","museum_cluster","park_small"], "temperate"),
|
| 25 |
-
"Bangalore": ("India", "BLR", ["park_large","market_outdoor","cafe_dense","residential_neighbourhood","shopping_street"], "tropical"),
|
| 26 |
-
"Lagos": ("Nigeria", "LOS", ["coastal_waterfront","market_outdoor","dense_urban_grid","food_market"], "tropical"),
|
| 27 |
-
"Seoul": ("South Korea", "SEO", ["dense_urban_grid","shopping_street","park_large","historic_district","narrow_alley_network"], "continental"),
|
| 28 |
-
"Copenhagen": ("Denmark", "CPH", ["coastal_waterfront","cafe_dense","bridge_pedestrian","market_covered","park_small"], "continental"),
|
| 29 |
-
"Lisbon": ("Portugal", "LIS", ["hill_or_elevation","narrow_alley_network","historic_district","coastal_waterfront","plaza_or_square"], "mediterranean"),
|
| 30 |
-
"Bogota": ("Colombia", "BOG", ["street_art_district","market_outdoor","park_large","historic_district","plaza_or_square"], "temperate"),
|
| 31 |
-
"Auckland": ("New Zealand", "AKL", ["coastal_waterfront","hill_or_elevation","park_large","market_outdoor","bridge_pedestrian"], "temperate"),
|
| 32 |
-
}
|
| 33 |
-
|
| 34 |
-
DENSITY = {
|
| 35 |
-
"Paris":"dense","Tokyo":"hyper_dense","New York City":"hyper_dense",
|
| 36 |
-
"Cape Town":"mixed","Marrakech":"dense","Buenos Aires":"dense",
|
| 37 |
-
"Mumbai":"hyper_dense","Berlin":"dense","Sydney":"mixed",
|
| 38 |
-
"Nairobi":"mixed","Istanbul":"dense","Mexico City":"hyper_dense",
|
| 39 |
-
"Amsterdam":"dense","Bangalore":"dense","Lagos":"hyper_dense",
|
| 40 |
-
"Seoul":"hyper_dense","Copenhagen":"mixed","Lisbon":"dense",
|
| 41 |
-
"Bogota":"dense","Auckland":"mixed",
|
| 42 |
-
}
|
| 43 |
|
| 44 |
# (duration_minutes, difficulty) β task count
|
| 45 |
TASK_COUNT = {
|
|
@@ -50,9 +21,6 @@ TASK_COUNT = {
|
|
| 50 |
(120,"easy"):8, (120,"medium"):10,(120,"hard"):12,
|
| 51 |
}
|
| 52 |
|
| 53 |
-
AREA_TYPES = ["city_center","historic_district","waterfront","park_district",
|
| 54 |
-
"mixed_residential","market_district","university_campus"]
|
| 55 |
-
AGE_GROUPS = ["children_only","teens","adults","mixed_family","mixed_adults"]
|
| 56 |
THEMES = ["observation","history","social","nature","urban_exploration","photography","logic"]
|
| 57 |
TASK_TYPES = ["find_and_photograph","observe_and_answer","collect_and_return",
|
| 58 |
"reach_and_verify","social_interaction","timed_challenge"]
|
|
@@ -137,23 +105,8 @@ def build_prompt(record: dict) -> str:
|
|
| 137 |
supervise = age in ("children_only","mixed_family")
|
| 138 |
max_task_time = int(dur * 0.80 // n_tasks) # generous per-task ceiling
|
| 139 |
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
has_religious = "religious_site_accessible" in loc["landscape_tags"]
|
| 143 |
-
|
| 144 |
-
exclusions = ["private_property","active_roadway","construction_sites",
|
| 145 |
-
"restricted_government_buildings"]
|
| 146 |
-
if has_water: exclusions.append("water_edge")
|
| 147 |
-
if has_religious: exclusions.append("religious_interiors")
|
| 148 |
-
|
| 149 |
-
climate_note = {
|
| 150 |
-
"tropical": "Tropical climate β note heat/humidity; require shade access within 10 min of each task.",
|
| 151 |
-
"arid": "Arid climate β mandatory water advisory; no sustained outdoor walk > 15 min per task.",
|
| 152 |
-
"mediterranean": "Mediterranean climate β no special constraint.",
|
| 153 |
-
"temperate": "Temperate climate β no special constraint.",
|
| 154 |
-
"continental": f"Continental climate β {'add cold-weather advisory (duration ' + str(dur) + ' min).' if dur >= 90 else 'no special constraint.'}",
|
| 155 |
-
"polar": "Polar climate β all outdoor tasks β€ 15 min each; mandatory warm-clothing note.",
|
| 156 |
-
}.get(loc["climate_zone"], "No special constraint.")
|
| 157 |
|
| 158 |
diff_rule = {
|
| 159 |
"easy": f"ALL {n_tasks} tasks easy (10 pts each). ZERO hard tasks.",
|
|
@@ -188,7 +141,7 @@ points scale : easy=10, medium=20, hard=30
|
|
| 188 |
max per-task time : {max_task_time} min (total task time MUST be β€ {int(dur*0.85)} min)
|
| 189 |
adult_supervision : {json.dumps(supervise)}
|
| 190 |
exclusion_zones : {json.dumps(exclusions)}
|
| 191 |
-
climate note : {
|
| 192 |
|
| 193 |
βββ ABSOLUTE RULES βββ
|
| 194 |
1. NO proper nouns anywhere β no city names, landmark names, street names, brand names.
|
|
|
|
| 4 |
and builds a prompt asking Gemini to write only the output for that input.
|
| 5 |
"""
|
| 6 |
import json
|
| 7 |
+
import os
|
| 8 |
import random
|
| 9 |
+
import sys
|
| 10 |
|
| 11 |
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
| 12 |
+
sys.path.insert(0, os.path.join(HERE, "..", "common"))
|
| 13 |
+
from cq_common import CITY_BANK, DENSITY, AREA_TYPES, AGE_GROUPS, compute_exclusions, climate_note
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
|
| 15 |
# (duration_minutes, difficulty) β task count
|
| 16 |
TASK_COUNT = {
|
|
|
|
| 21 |
(120,"easy"):8, (120,"medium"):10,(120,"hard"):12,
|
| 22 |
}
|
| 23 |
|
|
|
|
|
|
|
|
|
|
| 24 |
THEMES = ["observation","history","social","nature","urban_exploration","photography","logic"]
|
| 25 |
TASK_TYPES = ["find_and_photograph","observe_and_answer","collect_and_return",
|
| 26 |
"reach_and_verify","social_interaction","timed_challenge"]
|
|
|
|
| 105 |
supervise = age in ("children_only","mixed_family")
|
| 106 |
max_task_time = int(dur * 0.80 // n_tasks) # generous per-task ceiling
|
| 107 |
|
| 108 |
+
exclusions = compute_exclusions(loc["landscape_tags"])
|
| 109 |
+
climate_note_txt = climate_note(loc["climate_zone"], duration_minutes=dur, unit="task", unit_activity="walk")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
|
| 111 |
diff_rule = {
|
| 112 |
"easy": f"ALL {n_tasks} tasks easy (10 pts each). ZERO hard tasks.",
|
|
|
|
| 141 |
max per-task time : {max_task_time} min (total task time MUST be β€ {int(dur*0.85)} min)
|
| 142 |
adult_supervision : {json.dumps(supervise)}
|
| 143 |
exclusion_zones : {json.dumps(exclusions)}
|
| 144 |
+
climate note : {climate_note_txt}
|
| 145 |
|
| 146 |
βββ ABSOLUTE RULES βββ
|
| 147 |
1. NO proper nouns anywhere β no city names, landmark names, street names, brand names.
|
scripts/scavenger_hunt/validator.py
CHANGED
|
@@ -3,46 +3,17 @@ validator.py β scavenger_hunt
|
|
| 3 |
Validates one training example. Returns (is_valid: bool, errors: list[str]).
|
| 4 |
auto_fix() recomputes arithmetic fields in-place β never reject for math the model shouldn't be trusted with.
|
| 5 |
"""
|
| 6 |
-
import
|
|
|
|
| 7 |
from Levenshtein import distance as lev
|
| 8 |
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
"Mumbai","Berlin","Sydney","Nairobi","Istanbul","Mexico City","Amsterdam",
|
| 13 |
-
"Bangalore","Lagos","Seoul","Copenhagen","Lisbon","Bogota","Auckland",
|
| 14 |
-
# famous landmarks
|
| 15 |
-
"Eiffel","Louvre","Seine","Notre-Dame","Notre Dame","Montmartre",
|
| 16 |
-
"Shibuya","Senso-ji","Sensoji","Skytree","Harajuku",
|
| 17 |
-
"Central Park","Times Square","Brooklyn","Manhattan","Hudson",
|
| 18 |
-
"Table Mountain","Bosphorus","Hagia","Zocalo","ZΓ³calo",
|
| 19 |
-
"Han River","Gyeongbokgung","Nyhavn","Alfama","Tagus",
|
| 20 |
-
"La Boca","Recoleta","Gateway of India","Marine Drive",
|
| 21 |
-
"Brandenburg","Tiergarten","Opera House","Harbour Bridge","Bondi",
|
| 22 |
-
"Big Ben","Statue of Liberty","Golden Gate","Taj Mahal",
|
| 23 |
-
"Colosseum","Vatican","Kremlin","Buckingham","Westminster",
|
| 24 |
-
]
|
| 25 |
-
_NOUN_RE = re.compile(r"\b(" + "|".join(re.escape(n) for n in _NOUNS) + r")\b")
|
| 26 |
-
|
| 27 |
-
# ββ controlled tag vocabulary βββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 28 |
-
VALID_TAGS = {
|
| 29 |
-
"park_large","park_small","river_waterfront","lake_or_pond",
|
| 30 |
-
"coastal_waterfront","forest_urban","garden_formal","hill_or_elevation",
|
| 31 |
-
"dense_urban_grid","wide_boulevard","narrow_alley_network","market_outdoor",
|
| 32 |
-
"market_covered","plaza_or_square","bridge_pedestrian","train_station_major",
|
| 33 |
-
"port_or_harbour","iconic_landmark","historic_district",
|
| 34 |
-
"religious_site_accessible","museum_cluster","street_art_district",
|
| 35 |
-
"university_campus","shopping_street","cafe_dense","food_market",
|
| 36 |
-
"residential_neighbourhood",
|
| 37 |
-
}
|
| 38 |
|
| 39 |
POINTS = {"easy": 10, "medium": 20, "hard": 30}
|
| 40 |
|
| 41 |
|
| 42 |
-
def _nouns_in(text: str) -> list:
|
| 43 |
-
return _NOUN_RE.findall(text or "")
|
| 44 |
-
|
| 45 |
-
|
| 46 |
def validate_example(ex: dict) -> tuple[bool, list[str]]:
|
| 47 |
errors = []
|
| 48 |
|
|
|
|
| 3 |
Validates one training example. Returns (is_valid: bool, errors: list[str]).
|
| 4 |
auto_fix() recomputes arithmetic fields in-place β never reject for math the model shouldn't be trusted with.
|
| 5 |
"""
|
| 6 |
+
import os
|
| 7 |
+
import sys
|
| 8 |
from Levenshtein import distance as lev
|
| 9 |
|
| 10 |
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
| 11 |
+
sys.path.insert(0, os.path.join(HERE, "..", "common"))
|
| 12 |
+
from cq_common import VALID_TAGS, nouns_in as _nouns_in
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
POINTS = {"easy": 10, "medium": 20, "hard": 30}
|
| 15 |
|
| 16 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
def validate_example(ex: dict) -> tuple[bool, list[str]]:
|
| 18 |
errors = []
|
| 19 |
|