diff --git a/apps/web/scripts/.brave-progress.json b/apps/web/scripts/.brave-progress.json index eaeacf9..8dc17c6 100644 --- a/apps/web/scripts/.brave-progress.json +++ b/apps/web/scripts/.brave-progress.json @@ -3649,7 +3649,1757 @@ "apple-nectria-twig-blight-coral-spot", "apple-peniophora-root-canker", "apple-perennial-canker", - "apple-phomopsis-canker-fruit-decay-and-rough-bark" + "apple-phomopsis-canker-fruit-decay-and-rough-bark", + "apple-phymatotrichum-root-rot-cotton-root-rot", + "apple-phytophthora-crown-collar-and-root-rot-sprinkler-rot", + "apple-phytophthora-fruit-rot", + "apple-pink-mold-rot", + "apple-powdery-mildew", + "apple-rosellinia-root-rot-dematophora-root-rot", + "apple-rubber-rot", + "apple-american-hawthorne-rust", + "apple-cedar-apple-rust", + "apple-japanese-apple-rust", + "apple-pacific-coast-pear-rust", + "apple-quince-rust", + "apple-side-rot", + "apple-silver-leaf", + "apple-sooty-blotch-complex", + "apple-southern-blight", + "apple-thread-blight-hypochnus-leaf-blight", + "apple-valsa-canker", + "apple-violet-root-rot", + "apple-white-root-rot", + "apple-white-rot", + "apple-x-spot-nigrospora-spot", + "apple-zonate-leaf-spot", + "apricot-alternaria-spot-and-fruit-rot", + "apricot-armillaria-crown-and-root-rot-shoestring-crown-and-root-rot", + "apricot-brown-rot-blossom-and-twig-blight-and-fruit-rot", + "apricot-ceratocystis-canker", + "apricot-cytospora-canker", + "apricot-dematophora-root-rot", + "apricot-eutypa-dieback", + "apricot-green-fruit-rot", + "apricot-leaf-spot", + "apricot-phytophthora-crown-and-root-rot", + "apricot-phytophthora-pruning-wound-canker", + "apricot-powdery-mildew", + "apricot-replant-problems", + "apricot-rhizopus-fruit-rot", + "apricot-ripe-fruit-rot", + "apricot-scab", + "apricot-shot-hole", + "apricot-silver-leaf", + "apricot-verticillium-wilt", + "apricot-wood-rots-pathogenicity-has-not-been-proven-for-these-fungi", + "avocado-anthracnose", + "avocado-armillaria-root-rot-shoestring-root-rot", + "avocado-black-mildew", + "avocado-branch-canker", + "avocado-butt-rot", + "avocado-cercospora-spot-blotch", + "avocado-clitocybe-root-rot", + "avocado-collar-rot", + "avocado-dematophora-root-rot", + "avocado-dieback", + "avocado-fruit-rot-includes-stem-end-rot-fruit-spots", + "avocado-leaf-spots", + "avocado-phomopsis-spot", + "avocado-physalospora-canker", + "avocado-phytophthora-crown-rot", + "avocado-phytophthora-trunk-canker", + "avocado-phytophthora-root-rot", + "avocado-pink-rot", + "avocado-powdery-mildew", + "avocado-rhizoctonia-seed-and-root-rot", + "avocado-root-and-bark-rot", + "avocado-root-rot", + "avocado-rosellinia-root-rot", + "avocado-rusty-blight", + "avocado-scab-fruit-leaf", + "avocado-seedling-blight", + "avocado-smudgy-spot", + "avocado-sooty-blotch", + "avocado-tar-spot", + "avocado-verticillium-wilt", + "avocado-wood-rots", + "carrot-alternaria-leaf-blight", + "carrot-black-root-rot", + "carrot-black-rot-black-carrot-root-dieback", + "carrot-blue-mold-rot-blue-green-mold", + "carrot-brown-rot-phoma-disease", + "carrot-buckshot-rot", + "carrot-cavity-spot", + "carrot-cercospora-leaf-spot", + "carrot-cottony-rot", + "carrot-crater-rot", + "carrot-crown-rot", + "carrot-dieback-of-carrots", + "carrot-forking-brown-root", + "carrot-fusarium-dry-rot", + "carrot-gray-mold-rot", + "carrot-hard-rot", + "carrot-lateral-root-dieback", + "carrot-leaf-rot", + "carrot-leaf-spot", + "carrot-licorice-rot", + "carrot-phytophthora-root-rot", + "carrot-pink-mold-rot", + "carrot-powdery-mildew", + "carrot-pythium-brown-rot-and-forking", + "carrot-pythium-root-dieback", + "carrot-rhizoctonia-canker", + "carrot-rhizoctonia-seedling-disease", + "carrot-rhizopus-wooly-soft-rot", + "carrot-root-canker", + "carrot-root-dieback", + "carrot-root-rot", + "carrot-phymatotrichum-root-rot-cotton-root-rot", + "carrot-rubbery-brown-rot", + "carrot-rubbery-slate-rot", + "carrot-rusty-root", + "carrot-sclerotinia-rot", + "carrot-seed-mold", + "carrot-sooty-rot", + "carrot-sour-rot", + "carrot-southern-blight", + "carrot-stem-spot", + "carrot-tip-rot", + "carrot-umbel-blight", + "carrot-violet-root-rot", + "carrot-watery-soft-rot", + "citrus-alternaria-brown-spot", + "citrus-alternaria-leaf-spot-of-rough-lemon", + "citrus-alternaria-stem-end-rot", + "citrus-anthracnose-wither-tip", + "citrus-areolate-leaf-spot", + "citrus-black-root-rot", + "citrus-black-rot", + "citrus-black-spot", + "citrus-blue-mold", + "citrus-botrytis-blossom-and-twig-blight-gummosis", + "citrus-branch-knot", + "citrus-brown-rot-fruit", + "citrus-charcoal-root-rot", + "citrus-damping-off", + "citrus-diplodia-gummosis-and-stem-end-rot", + "citrus-dothiorella-gummosis-and-rot", + "citrus-dry-root-rot-complex", + "citrus-dry-rot-fruit", + "citrus-fly-speck", + "citrus-fusarium-rot-fruit", + "citrus-fusarium-wilt", + "citrus-gray-mold-fruit", + "citrus-greasy-spot-and-greasy-spot-rind-blotch", + "citrus-green-mold", + "citrus-hendersonula-branch-wilt", + "citrus-leaf-spot", + "citrus-mal-secco", + "citrus-mancha-foliar-de-los-citricos", + "citrus-melanose", + "citrus-mucor-fruit-rot", + "citrus-mushroom-root-rot-shoestring-root-rot-or-oak-root-fungus", + "citrus-phaeoramularia-leaf-and-fruit-spot", + "citrus-phymatotrichum-root-rot", + "citrus-phytophthora-foot-rot-gummosis-and", + "citrus-pink-disease", + "citrus-pink-mold", + "citrus-pleospora-rot", + "citrus-poria-root-rot", + "citrus-post-bloom-fruit-drop", + "citrus-powdery-mildew", + "citrus-rhizopus-rot", + "citrus-rio-grande-gummosis", + "citrus-rootlet-rot", + "citrus-rosellinia-root-rot", + "citrus-scab", + "citrus-sclerotinia-twig-blight-fruit-rot-and-root-rot", + "citrus-septoria-spot", + "citrus-sooty-blotch", + "citrus-sour-rot", + "citrus-sweet-orange-scab", + "citrus-thread-blight", + "citrus-trichoderma-rot", + "citrus-twig-blight", + "citrus-ustulina-root-rot", + "citrus-whisker-mold", + "citrus-white-root-rot", + "coconut-algal-leaf-spot", + "coconut-anthracnose", + "coconut-bitten-leaf", + "coconut-bipolaris-leaf-spot", + "coconut-black-scorch", + "coconut-bud-rot", + "coconut-catacauma-leaf-spot", + "coconut-dry-basal-rot", + "coconut-ganoderma-butt-rot", + "coconut-graphiola-leaf-spot", + "coconut-gray-leaf-blight", + "coconut-koleroga", + "coconut-leaf-blight", + "coconut-leaf-spots", + "coconut-lethal-bole-rot", + "coconut-lixa-grande", + "coconut-lixa-pequea", + "coconut-nut-fall", + "coconut-powdery-mildew", + "coconut-queima-das-folhas", + "coconut-root-rot", + "coconut-stem-bleeding", + "coconut-stigmina-leaf-spot", + "coconut-thread-blight", + "coffee-anthracnose", + "coffee-armillaria-root-rot", + "coffee-algal-red-leaf-spot", + "coffee-bark-disease", + "coffee-berry-blotch", + "coffee-black-rosellinia-root-rot", + "coffee-black-seedling-root-rot", + "coffee-brown-blight", + "coffee-brown-eye-spot", + "coffee-brown-leaf-spot", + "coffee-collar-rot", + "coffee-coffee-berry-disease", + "coffee-die-back", + "coffee-dry-root-rot", + "coffee-leaf-blight", + "coffee-leaf-spot", + "coffee-pink-disease", + "coffee-red-blister-disease-robusta-coffee", + "coffee-red-root-rot", + "coffee-rust-orange-or-leaf-rust", + "coffee-rust-powdery-or-grey-rust", + "coffee-south-america-leaf-spot", + "coffee-thread-blight", + "coffee-tip-blast", + "coffee-tracheomycosis-wilt", + "coffee-wilt", + "coffee-warty-berry", + "corn-anthracnose-leaf-blight-anthracnose-stalk-rot", + "corn-aspergillus-ear-and-kernel-rot", + "corn-banded-leaf-and-sheath-spot", + "corn-black-bundle-disease", + "corn-black-kernel-rot", + "corn-borde-blanco", + "corn-brown-spot-black-spot-stalk-rot", + "corn-cephalosporium-kernel-rot", + "corn-charcoal-rot", + "corn-corticium-ear-rot", + "corn-curvularia-leaf-spot", + "corn-didymella-leaf-spot", + "corn-diplodia-ear-rot-and-stalk-rot", + "corn-diplodia-ear-rot-stalk-rot-seed-rot-seedling-blight", + "corn-diplodia-leaf-spot-or-leaf-streak", + "corn-brown-stripe-downy-mildew", + "corn-crazy-top-downy-mildew", + "corn-green-ear-downy-mildew-graminicola-downy-mildew", + "corn-java-downy-mildew", + "corn-philippine-downy-mildew", + "corn-sorghum-downy-mildew", + "corn-spontaneum-downy-mildew", + "corn-sugarcane-downy-mildew", + "corn-dry-ear-rot-cob-kernel-and-stalk-rot", + "corn-ear-rots-minor", + "corn-ergot-horses-tooth", + "corn-eyespot", + "corn-fusarium-ear-and-stalk-rot", + "corn-fusarium-kernel-root-and-stalk-rot-seed-rot-and-seedling-blight", + "corn-fusarium-stalk-rot-seedling-root-rot", + "corn-gibberella-ear-and-stalk-rot", + "corn-gray-ear-rot", + "corn-corn-grey-leaf-spot-gray-leaf-spot-cercospora-leaf-spot", + "corn-helminthosporium-root-rot", + "corn-hormodendrum-ear-rot-cladosporium-ear-rot", + "corn-hyalothyridium-leaf-spot", + "corn-late-wilt", + "corn-leaf-spots-minor", + "corn-northern-corn-leaf-blight-white-blast-crown-stalk-rot-stripe", + "corn-northern-corn-leaf-spot-helminthosporium-ear-rot-race-1", + "corn-penicillium-ear-rot-blue-eye-blue-mold", + "corn-phaeocytostroma-stalk-rot-and-root-rot", + "corn-phaeosphaeria-leaf-spot", + "corn-physalospora-ear-rot-botryosphaeria-ear-rot", + "corn-pyrenochaeta-stalk-rot-and-root-rot", + "corn-pythium-root-rot", + "corn-pythium-stalk-rot", + "corn-red-kernel-disease-ear-mold-leaf-and-seed-rot", + "corn-rhizoctonia-ear-rot-sclerotial-rot", + "corn-rhizoctonia-root-rot-and-stalk-rot", + "corn-root-rots-minor", + "corn-rostratum-leaf-spot-helminthosporium-leaf-disease-ear-and-stalk-rot", + "corn-rust-common-corn", + "corn-rust-southern-corn", + "corn-rust-tropical-corn", + "corn-sclerotium-ear-rot-southern-blight", + "corn-selenophoma-leaf-spot", + "corn-sheath-rot", + "corn-shuck-rot", + "corn-silage-mold", + "corn-smut-common", + "corn-smut-false", + "corn-smut-head", + "corn-southern-corn-leaf-blight-and-stalk-rot", + "corn-southern-leaf-spot", + "corn-stalk-rots-minor", + "corn-storage-rots", + "corn-tar-spot", + "corn-trichoderma-ear-rot-and-root-rot", + "corn-white-ear-rot-root-and-stalk-rot", + "corn-yellow-leaf-blight", + "corn-zonate-leaf-spot", + "cucumber-alternaria-leaf-blight", + "cucumber-alternaria-leaf-spot", + "cucumber-anthracnose-stem-leaf-and-fruit", + "cucumber-belly-rot", + "cucumber-black-root-rot", + "cucumber-blue-mold-rot", + "cucumber-cephalosporium-root-and-hypocotyl-rot-stem-streak-and-dieback", + "cucumber-cercospora-leaf-spot", + "cucumber-charcoal-rot-vine-decline-and-fruit-rot", + "cucumber-choanephora-fruit-rot", + "cucumber-collapse-of-melon", + "cucumber-corynespora-blighttarget-spot", + "cucumber-crater-rot-fruit", + "cucumber-crown-and-foot-rot", + "cucumber-damping-off", + "cucumber-fusarium-fruit-rot", + "cucumber-fusarium-wilt", + "cucumber-gray-mold", + "cucumber-gummy-stem-blight-vine-decline", + "cucumber-lasiodiplodia-vine-declinefruit-rot", + "cucumber-monosporascus-root-rotmyrothecium-canker-black-canker", + "cucumber-net-spot", + "cucumber-phoma-blight", + "cucumber-purple-stem", + "cucumber-phomopsis-black-stem", + "cucumber-phyllosticta-leaf-spot", + "cucumber-phytophthora-root-rot", + "cucumber-pink-mold-rot", + "cucumber-plectosporium-blight", + "cucumber-pythium-fruit-rot-cottony-leak", + "cucumber-rhizopus-soft-rot-fruit", + "cucumber-scabgummosis", + "cucumber-sclerotinia-stem-rot", + "cucumber-septoria-leaf-blight", + "cucumber-southern-blight-sclerotium-fruit-and-stem-rot", + "cucumber-sudden-wilt", + "cucumber-ulocladium-leaf-spot", + "cucumber-verticillium-wilt", + "cucumber-web-blight", + "grape-alternaria-rot", + "grape-angular-leaf-scorch", + "grape-angular-leaf-spot", + "grape-anthracnose-and-birds-eye-rot", + "grape-armillaria-root-rot-shoestring-root-rot", + "grape-aspergillus-rot", + "grape-black-rot-of-grapes", + "grape-botrytis-grey-rot-or-noble-rot", + "grape-bot-canker", + "grape-ripe-rot", + "lettuce-alternaria-leaf-spot", + "lettuce-anthracnose", + "lettuce-bottom-rot", + "lettuce-cercospora-leaf-spot", + "lettuce-damping-off-pythium", + "lettuce-damping-off-rhizoctonia", + "lettuce-drop-sclerotinia-rot", + "lettuce-gray-mold", + "lettuce-phymatotrichum-root-rot-cotton-root-rot", + "lettuce-powdery-mildew", + "lettuce-septoria-leaf-spot", + "lettuce-southern-blight", + "lettuce-stemphylium-leaf-spot", + "lettuce-wilt-and-leaf-blight", + "mango-alternaria-leaf-spots", + "mango-anthracnoseref-nameploetz-2003", + "mango-black-banded-disease", + "mango-black-mildew", + "mango-black-mold-rot", + "mango-black-rot", + "mango-blossom-blight", + "mango-blue-mold", + "mango-branch-canker", + "mango-branch-necrosis", + "mango-ceratocystis-wilt", + "mango-charcoal-fruit-rot", + "mango-charcoal-root-rot", + "mango-crown-rot", + "mango-crusty-leaf-spot", + "mango-curvularia-blight", + "mango-dieback", + "mango-felt-fungus", + "mango-fruit-rot", + "mango-gray-leaf-spot", + "mango-hendersonia-rot", + "mango-leaf-blight", + "mango-leaf-spot", + "mango-macrophoma-rot", + "mango-mango-malformation", + "mango-mucor-rot", + "mango-mushroom-root-rot", + "mango-phoma-blight", + "mango-phyllosticta-leaf-spot", + "mango-pink-disease", + "mango-powdery-mildew", + "mango-rhizopus-rot", + "mango-root-rot", + "mango-scab", + "mango-sclerotinia-rot", + "mango-seed-rots", + "mango-shoestring-rot", + "mango-sooty-blotch", + "mango-sooty-molds", + "mango-stem-canker", + "mango-stem-end-rot", + "mango-stem-gall", + "mango-stemphylium-rot", + "mango-stigmina-leaf-spot", + "mango-tip-dieback", + "mango-transit-rot", + "mango-trunk-rot", + "mango-twig-blight", + "mango-verticillium-wilt", + "mango-white-sooty-blotch", + "mango-wood-rot", + "papaya-alternaria-fruit-spot", + "papaya-angular-leaf-spot", + "papaya-anthracnose", + "papaya-black-spot", + "papaya-blossom-spot", + "papaya-black-rot", + "papaya-brown-spot", + "papaya-chocolate-spot", + "papaya-collar-rot", + "papaya-foot-rot", + "papaya-fruit-rot", + "papaya-fruit-spot", + "papaya-fusarium-fruit-rot", + "papaya-guignardia-spot", + "papaya-greasy-spot", + "papaya-internal-blight", + "papaya-lasiodiplodia-fruit-rot", + "papaya-leaf-spot", + "papaya-petiole-spot", + "papaya-phytophthora-blight", + "papaya-powdery-mildew", + "papaya-phytophthora-fruit-rot", + "papaya-rhizopus-soft-rot", + "papaya-root-rot", + "papaya-sclerotium-blight", + "papaya-seedling-blight", + "papaya-stem-end-rot", + "papaya-stemphylium-fruit-spot", + "papaya-stem-rot", + "papaya-target-spot", + "papaya-verticillium-wilt", + "papaya-wet-fruit-rot", + "papaya-", + "peanut-alternaria-leaf-blight", + "peanut-alternaria-leaf-spot", + "peanut-alternaria-spot-and-veinal-necrosis", + "peanut-anthracnose", + "peanut-aspergillus-crown-rot", + "peanut-blackhull", + "peanut-botrytis-blight", + "peanut-charcoal-rot-and-macrophomina-leaf-spot", + "peanut-choanephora-leaf-spot", + "peanut-collar-rot", + "peanut-colletotrichum-leaf-spot", + "peanut-cylindrocladium-black-rot", + "peanut-cylindrocladium-leaf-spot", + "peanut-damping-off-aspergillus", + "peanut-damping-off-fusarium", + "peanut-damping-off-pythium", + "peanut-damping-off-rhizoctonia", + "peanut-damping-off-rhizopus", + "peanut-drechslera-leaf-spot", + "peanut-fusarium-peg-and-root-rot", + "peanut-fusarium-wilt", + "peanut-leaf-spot-early", + "peanut-leaf-spot-late", + "peanut-myrothecium-leaf-blight", + "peanut-olpidium-root-rot", + "peanut-pepper-spot-and-scorch", + "peanut-pestalotiopsis-leaf-spot", + "peanut-phoma-leaf-blight", + "peanut-phomopsis-foliar-blight", + "peanut-phomopsis-leaf-spot", + "peanut-phyllosticta-leaf-spot", + "peanut-phymatotrichum-root-rot", + "peanut-pod-rot-pod-breakdown", + "peanut-powdery-mildew", + "peanut-pythium-peg-and-root-rot", + "peanut-pythium-wilt", + "peanut-rhizoctonia-foliar-blight-peg-and-root-rot", + "peanut-scab", + "peanut-sclerotinia-blight", + "peanut-stem-rot-southern-blight", + "peanut-verticillium-wilt", + "peanut-web-blotch-net-blotch", + "peanut-yellow-mold", + "peanut-zonate-leaf-spot", + "pear-alternaria-fruit-rot", + "pear-anthracnose-canker-and-bulls-eye-rot", + "pear-armillaria-root-rot-shoestring-root-rot", + "pear-bitter-rot", + "pear-black-rot-leaf-spot-and-canker", + "pear-black-spot-of-japanese-pear", + "pear-blister-canker", + "pear-blister-disease", + "pear-blue-mold-rot", + "pear-botrytis-spur-and-blossom-blight", + "pear-brown-rot", + "pear-cladosporium-fruit-rot", + "pear-clitocybe-root-rot-mushroom-root-rot", + "pear-coprinus-rot", + "pear-dematophora-root-rot-rosellinia-root-rot", + "pear-diplodia-canker", + "pear-elsinoe-leaf-and-fruit-spot", + "pear-european-canker", + "pear-fabraea-leaf-and-fruit-spot", + "pear-fly-speck", + "pear-gibberella-canker", + "pear-gray-mold-rot", + "pear-late-leaf-spot", + "pear-mucor-fruit-rot", + "pear-mycosphaerella-leaf-spot-ashy-leaf-spot-and-fruit-spot", + "pear-nectria-twig-blight-coral-spot", + "pear-pear-scab", + "pear-perennial-canker", + "pear-phyllosticta-leaf-spot", + "pear-phytophthora-crown-and-root-rot-sprinkler-rot", + "pear-pink-mold-rot", + "pear-powdery-mildew", + "pear-pythium-dieback", + "pear-rhizopus-rot", + "pear-rust-american-hawthorne", + "pear-rust-kerns-pear", + "pear-rust-pacific-coast-pear", + "pear-rust-pear-trellis-european-pear-rust", + "pear-rust-rocky-mountain-pear", + "pear-side-rot", + "pear-silver-leaf", + "pear-sooty-blotch", + "pear-thread-blight-hypochnus-leaf-blight", + "pear-valsa-canker", + "pear-wood-rot", + "pear-xylaria-root-rot", + "pepper-alternaria-stem-canker", + "pepper-anthracnose", + "pepper-black-mold-rot", + "pepper-black-root-rot", + "pepper-black-shoulder", + "pepper-buckeye-rot-of-tomato", + "pepper-cercospora-leaf-mold", + "pepper-charcoal-rot", + "pepper-corky-root-rot", + "pepper-didymella-stem-rot", + "pepper-early-blight", + "pepper-fusarium-crown-and-root-rot", + "pepper-fusarium-wilt", + "pepper-gray-leaf-spot", + "pepper-gray-mold", + "pepper-late-blight", + "pepper-leaf-mold", + "pepper-phoma-rot", + "pepper-pythium-damping-off-and-fruit-rot", + "pepper-rhizoctonia-damping-off-and-fruit-rot", + "pepper-rhizopus-rot", + "pepper-septoria-leaf-spot", + "pepper-sour-rot", + "pepper-southern-blight", + "pepper-target-spot", + "pepper-verticillium-wilt", + "pepper-white-mold", + "pineapple-anthracnose", + "pineapple-pineapple-black-rot", + "pineapple-leaf-spot", + "pineapple-root-rot", + "pineapple-seedling-blight", + "pineapple-white-leaf-spot", + "pineapple-aspergillus-rot", + "pineapple-botryodiplodia-rot", + "pineapple-black-rot-water-blister", + "pineapple-fusariosis-gummosis", + "pineapple-glassy-spoilage", + "pineapple-hendersonula-fruit-rot", + "pineapple-interfruitlet-corking", + "pineapple-leathery-pocket", + "pineapple-nigrospora-fruit-rot", + "pineapple-rhizopus-rot", + "pineapple-yeasty-fermentation", + "rice-aggregate-sheath", + "rice-black-horse-riding", + "rice-blast-leaf-neck-rotten-neck-nodal-and-collar", + "rice-brown-spot", + "rice-crown-sheath-rot", + "rice-eyespot", + "rice-false-smut", + "rice-kernel-smut", + "rice-leaf-smut", + "rice-leaf-scald", + "rice-narrow-brown-leaf-spot", + "rice-root-rots", + "rice-seedling-blight", + "rice-sheath-blight", + "rice-sheath-rot", + "rice-sheath-spot", + "rice-stem-rot", + "rice-water-mold-seed-rot-and-seedling-disease", + "sorghum-acremonium-wilt", + "sorghum-anthracnose-foliar-head-root-and-stalk-rot", + "sorghum-charcoal-rot", + "sorghum-crazy-top-downy-mildew", + "sorghum-damping-off-and-seed-rot", + "sorghum-fusarium-head-blight-root-and-stalk-rot", + "sorghum-grain-storage-mold", + "sorghum-gray-leaf-spot", + "sorghum-latter-leaf-spot", + "sorghum-leaf-blight", + "sorghum-milo-disease-periconia-root-rot", + "sorghum-oval-leaf-spot", + "sorghum-pokkah-boeng-twisted-top", + "sorghum-pythium-root-rot", + "sorghum-rough-leaf-spot", + "sorghum-seedling-blight-and-seed-rot", + "sorghum-smut-covered-kernel", + "sorghum-smut-head", + "sorghum-smut-loose-kernel", + "sorghum-sooty-stripe", + "sorghum-sorghum-downy-mildew", + "sorghum-tar-spot", + "sorghum-target-leaf-spot", + "sorghum-zonate-leaf-spot-and-sheath-blight", + "soybean-alternaria-leaf-spot", + "soybean-anthracnose", + "soybean-black-leaf-blight", + "soybean-black-root-rot", + "soybean-brown-spot", + "soybean-brown-stem-rot", + "soybean-charcoal-rotref-namesoybeanok", + "soybean-choanephora-leaf-blight", + "soybean-damping-off", + "soybean-drechslera-blight", + "soybean-frogeye-leaf-spot", + "soybean-fusarium-root-rot", + "soybean-leptosphaerulina-leaf-spot", + "soybean-mycoleptodiscus-root-rot", + "soybean-neocosmospora-stem-rot", + "soybean-phomopsis-seed-decay", + "soybean-phytophthora-root-and-stem-rot", + "soybean-phyllosticta-leaf-spot", + "soybean-phymatotrichum-root-rot-cotton-root-rot", + "soybean-pod-and-stem-blight", + "soybean-powdery-mildew", + "soybean-purple-seed-stain", + "soybean-pyrenochaeta-leaf-spot", + "soybean-pythium-rot", + "soybean-red-crown-rot", + "soybean-red-leaf-blotch-dactuliophora-leaf-spot", + "soybean-rhizoctonia-aerial-blight", + "soybean-rhizoctonia-root-and-stem-rot", + "soybean-scab", + "soybean-sclerotinia-stem-rot", + "soybean-southern-blight-damping-off-and-stem-rot-sclerotium-blightref-namesoybeanok", + "soybean-stem-canker", + "soybean-stemphylium-leaf-blight", + "soybean-sudden-death-syndrome", + "soybean-target-spot", + "soybean-yeast-spot", + "spinach-anthracnose", + "spinach-aphanomyces-root-rot", + "spinach-cercospora-leaf-spot", + "spinach-damping-off", + "spinach-downy-mildew-blue-mold", + "spinach-fusarium-wilt", + "spinach-leaf-spot", + "spinach-phoma-blight", + "spinach-phytophthora-root-rot", + "spinach-pythium-root-rot", + "spinach-red-rust", + "spinach-seed-mold", + "spinach-white-rust", + "spinach-white-smut", + "strawberry-alternaria-fruit-rot", + "strawberry-anther-and-pistil-blight", + "strawberry-anthracnose-and-anthracnose-fruit-rot-and-black-spot", + "strawberry-armillaria-crown-and-root-rot-shoestring-crown-and-root-rot", + "strawberry-black-leaf-spot", + "strawberry-black-root-rot-disease-complex", + "strawberry-cercospora-leaf-spot", + "strawberry-charcoal-rot", + "strawberry-coniothyrium-diseases", + "strawberry-dematophora-crown-and-root-rot-white-root-rot", + "strawberry-diplodina-rot-leaf-and-stalk-rot", + "strawberry-fruit-rots-in-addition-to-those-appearing-elsewhere-in-this-listing", + "strawberry-byssochlamys-rot", + "strawberry-brown-cap", + "strawberry-fruit-blotch", + "strawberry-gray-mold-leaf-blight-and-dry-crown-rot", + "strawberry-hainesia-leaf-spot", + "strawberry-hard-brown-rot", + "strawberry-leaf-blotch", + "strawberry-leaf-rust", + "strawberry-leather-rot", + "strawberry-lilac-soft-rot", + "strawberry-pestalotia-fruit-rot", + "strawberry-leaf-blight", + "strawberry-postharvest-rots", + "strawberry-phytophthora-crown-and-root-rot", + "strawberry-botrytis-crown-rot", + "strawberry-gray-sterile-fungus-root-rot", + "strawberry-idriella-root-rot", + "strawberry-macrophomina-root-rot", + "strawberry-olpidium-root-infection", + "strawberry-synchytrium-root-gall", + "strawberry-purple-leaf-spot", + "strawberry-red-stele", + "strawberry-rhizoctonia-bud-and-crown-rot-leaf-blight-web-blight-fruit-rot", + "strawberry-rhizopus-rot-leak", + "strawberry-sclerotinia-crown-and-fruit-rot", + "strawberry-septoria-hard-rot-and-leaf-spot", + "strawberry-stunt-pythium-root-rot", + "strawberry-southern-blight-sclerotium-rot", + "strawberry-stem-end-rot", + "strawberry-tan-brown-rot-of-fruit", + "strawberry-verticillium-wilt", + "sugarcane-banded-sclerotial-leaf-disease", + "sugarcane-black-rot", + "sugarcane-black-stripe", + "sugarcane-brown-spot", + "sugarcane-brown-stripe", + "sugarcane-downy-mildew-leaf-splitting-form", + "sugarcane-eye-spot", + "sugarcane-fusarium-sett-and-stem-rot", + "sugarcane-iliau", + "sugarcane-leaf-blast", + "sugarcane-leaf-blight", + "sugarcane-leaf-scorch", + "sugarcane-marasmius-sheath-and-shoot-blight", + "sugarcane-myriogenospora-leaf-binding-tangle-top", + "sugarcane-phyllosticta-leaf-spot", + "sugarcane-phytophthora-rot-of-cuttings", + "sugarcane-pineapple-disease", + "sugarcane-pokkah-boeng-that-may-have-knife-cut-symptoms", + "sugarcane-red-leaf-spot-purple-spot", + "sugarcane-red-rot-of-leaf-sheath-and-sprout-rot", + "sugarcane-red-spot-of-leaf-sheath", + "sugarcane-rhizoctonia-sheath-and-shoot-rot", + "sugarcane-rind-disease-sour-rot", + "sugarcane-ring-spot", + "sugarcane-root-rots", + "sugarcane-rust-common", + "sugarcane-rust-orange", + "sugarcane-schizophyllum-rot", + "sugarcane-sclerophthora-disease", + "sugarcane-seedling-blight", + "sugarcane-sheath-rot", + "sugarcane-smut-culmicolous", + "sugarcane-target-blotch", + "sugarcane-veneer-blotch", + "sugarcane-white-rash", + "sugarcane-wilt", + "sugarcane-yellow-spot", + "sugarcane-zonate-leaf-spot", + "sunflower-alternaria-leaf-blight-stem-spot-and-head-rot", + "sunflower-botrytis-head-rot-gray-mold", + "sunflower-charcoal-rot", + "sunflower-fusarium-stalk-rot", + "sunflower-fusarium-wilt", + "sunflower-myrothecium-leaf-and-stem-spot", + "sunflower-phialophora-yellows", + "sunflower-phoma-black-stem", + "sunflower-phomopsis-brown-stem-canker", + "sunflower-phymatotrichum-root-rot-cotton-root-rot", + "sunflower-phytophthora-stem-rot", + "sunflower-pythium-seedling-blight-and-root-rot", + "sunflower-rhizoctonia-seedling-blight", + "sunflower-rhizopus-head-rot", + "sunflower-sclerotinia-basal-stalk-rot-and-wilt-mid-stalk-rot-head-rot", + "sunflower-sclerotinia-basal-stalk-rot-and-wilt", + "sunflower-sclerotium-basal-stalk-and-root-rot-southern-blight", + "sunflower-septoria-leaf-spot", + "sunflower-verticillium-wilt", + "sunflower-white-rust", + "sunflower-yellow-rust", + "sweet-potato-alternaria-leaf-spot-and-stem-blight", + "sweet-potato-alternaria-storage-rot", + "sweet-potato-black-rot", + "sweet-potato-blue-mold-rot", + "sweet-potato-cercospora-leaf-spot", + "sweet-potato-charcoal-rot", + "sweet-potato-chlorotic-leaf-distortion", + "sweet-potato-circular-spot", + "sweet-potato-end-rot", + "sweet-potato-false-broom-rape", + "sweet-potato-foot-rot", + "sweet-potato-fusarium-root-rot-and-stem-canker", + "sweet-potato-fusarium-wilt-stem-rot", + "sweet-potato-gray-mold-rot", + "sweet-potato-java-black-rot", + "sweet-potato-leaf-mold", + "sweet-potato-mottle-necrosis", + "sweet-potato-phyllosticta-leaf-blight", + "sweet-potato-phymatotrichum-root-rot-cotton-root-rot", + "sweet-potato-pink-root", + "sweet-potato-punky-rot", + "sweet-potato-rhizoctonia-stem-canker-sprout-rot", + "sweet-potato-rhizopus-soft-rot", + "sweet-potato-rootlet-rot", + "sweet-potato-rust-red", + "sweet-potato-rust-white", + "sweet-potato-scab-leaf-and-stem", + "sweet-potato-southern-blight-sclerotial-blight", + "sweet-potato-scurf", + "sweet-potato-septoria-leaf-spot", + "sweet-potato-storage-rot", + "sweet-potato-surface-rot", + "sweet-potato-violet-root-rot", + "sweet-potato-", + "tobacco-anthracnose", + "tobacco-barn-spot", + "tobacco-barn-rot", + "tobacco-black-root-rot", + "tobacco-blue-mold-downy-mildew", + "tobacco-brown-spot", + "tobacco-charcoal-rot", + "tobacco-collar-rot", + "tobacco-damping-off-pythium", + "tobacco-frogeye-leaf-spot", + "tobacco-fusarium-wilt", + "tobacco-gray-mold", + "tobacco-mycosphaerella-leaf-spot", + "tobacco-olpidium-seedling-blight", + "tobacco-phyllosticta-leaf-spot", + "tobacco-powdery-mildew", + "tobacco-ragged-leaf-spot", + "tobacco-scab", + "tobacco-sore-shin-and-damping-off", + "tobacco-southern-stem-rot-southern-blight", + "tobacco-stem-rot-of-transplants", + "tobacco-target-spot", + "tobacco-verticillium-wilt", + "watermelon-alternaria-leaf-blight", + "watermelon-alternaria-leaf-spot", + "watermelon-anthracnose-stem-leaf-and-fruit", + "watermelon-belly-rot", + "watermelon-black-root-rot", + "watermelon-blue-mold-rot", + "watermelon-cephalosporium-root-and-hypocotyl-rot-stem-streak-and-dieback", + "watermelon-cercospora-leaf-spot", + "watermelon-charcoal-rot-vine-decline-and-fruit-rot", + "watermelon-choanephora-fruit-rot", + "watermelon-collapse-of-melon", + "watermelon-corynespora-blighttarget-spot", + "watermelon-crater-rot-fruit", + "watermelon-crown-and-foot-rot", + "watermelon-damping-off", + "watermelon-fusarium-fruit-rot", + "watermelon-fusarium-wilt", + "watermelon-gray-mold", + "watermelon-gummy-stem-blight-vine-decline", + "watermelon-lasiodiplodia-vine-declinefruit-rot", + "watermelon-monosporascus-root-rotmyrothecium-canker-black-canker", + "watermelon-net-spot", + "watermelon-phoma-blight", + "watermelon-purple-stem", + "watermelon-phomopsis-black-stem", + "watermelon-phyllosticta-leaf-spot", + "watermelon-phytophthora-root-rot", + "watermelon-pink-mold-rot", + "watermelon-plectosporium-blight", + "watermelon-powdery-mildew", + "watermelon-pythium-fruit-rot-cottony-leak", + "watermelon-rhizopus-soft-rot-fruit", + "watermelon-scabgummosis", + "watermelon-sclerotinia-stem-rot", + "watermelon-septoria-leaf-blight", + "watermelon-southern-blight-sclerotium-fruit-and-stem-rot", + "watermelon-sudden-wilt", + "watermelon-ulocladium-leaf-spot", + "watermelon-verticillium-wilt", + "watermelon-web-blight", + "wheat-alternaria-leaf-blight", + "wheat-anthracnose", + "wheat-ascochyta-leaf-spot", + "wheat-aureobasidium-decay", + "wheat-black-head-molds-sooty-molds", + "wheat-black-point-kernel-smudge", + "wheat-cephalosporium-stripe", + "wheat-cottony-snow-mold", + "wheat-crown-rot-of-wheatcrown-rot-foot-rot-seedling-blight-dryland-root-rot", + "wheat-dilophospora-leaf-spot-twist", + "wheat-downy-mildew-crazy-top", + "wheat-dwarf-bunt", + "wheat-eyespot-foot-rot-strawbreaker", + "wheat-false-eyespot", + "wheat-flag-smut", + "wheat-foot-rot-dryland-foot-rot", + "wheat-halo-spot", + "wheat-karnal-bunt-partial-bunt", + "wheat-leaf-rust-brown-rust", + "wheat-leptosphaeria-leaf-spot", + "wheat-loose-smut", + "wheat-microscopica-leaf-spot", + "wheat-phoma-spot", + "wheat-pink-snow-mold-fusarium-patch", + "wheat-platyspora-leaf-spot", + "wheat-powdery-mildew", + "wheat-pythium-root-rot", + "wheat-rhizoctonia-root-rot", + "wheat-ring-spot-wirrega-blotch", + "wheat-scab-head-blight-fusarium-head-blight-fhb", + "wheat-sclerotinia-snow-mold-snow-scald", + "wheat-sclerotium-wilt-see-southern-blight", + "wheat-septoria-blotch", + "wheat-sharp-eyespot", + "wheat-snow-rot", + "wheat-southern-blight-sclerotium-base-rot", + "wheat-speckled-snow-mold-gray-snow-mold-or-typhula-blight", + "wheat-spot-blotch", + "wheat-stagonospora-blotchref-nametam-blotch", + "wheat-stem-rust-black-rust", + "wheat-storage-molds", + "wheat-stripe-rust-yellow-rust", + "wheat-tan-spot-yellow-leaf-spot-red-smudge", + "wheat-tar-spot", + "wheat-wheat-blast", + "wheat-zoosporic-root-rot", + "alfalfa-acrocalymma-root-and-crown-rot", + "alfalfa-anthracnose", + "alfalfa-aphanomyces-root-rot", + "alfalfa-black-patch", + "alfalfa-black-root-rot", + "alfalfa-blossom-blight", + "alfalfa-brown-root-rot", + "alfalfa-charcoal-rot", + "alfalfa-corky-root-rot", + "alfalfa-crown-wart", + "alfalfa-cylindrocarpon-root-rot", + "alfalfa-cylindrocladium-root-and-crown-rot", + "alfalfa-damping-off", + "alfalfa-fusarium-wilt", + "alfalfa-lepto-leaf-spot", + "alfalfa-marasmius-root-rot", + "alfalfa-mycoleptodiscus-crown-and-root-rot", + "alfalfa-myrothecium-root-rot", + "alfalfa-phymatotrichum-root-rot-cotton-root-rot-texas-root-rot", + "alfalfa-phytophthora-root-rot", + "alfalfa-powdery-mildew", + "alfalfa-rhizoctonia-root-rot-and-stem-blight", + "alfalfa-rhizopus-sprout-rot", + "alfalfa-sclerotinia-crown-and-stem-rot", + "alfalfa-southern-blight", + "alfalfa-spring-black-stem-and-leaf-spot", + "alfalfa-stagonospora-leaf-spot-and-root-rot", + "alfalfa-stemphylium-leaf-spot", + "alfalfa-summer-black-stem-and-leaf-spot", + "alfalfa-verticillium-wilt", + "alfalfa-violet-root-rot", + "alfalfa-winter-crown-rot-coprinus-snow-mold", + "alfalfa-yellow-leaf-blotch", + "asparagus-anthracnose", + "asparagus-ascochyta-blight", + "asparagus-blue-mold-rot", + "asparagus-cercospora-blight", + "asparagus-dead-stem", + "asparagus-fusarium-crown-and-root-rot", + "asparagus-fusarium-spear-spot", + "asparagus-gray-mold-shoot-blight", + "asparagus-leaf-spot", + "asparagus-phomopsis-blight", + "asparagus-phytophthora-spear-and-crown-rot", + "asparagus-purple-spot", + "asparagus-rhizoctonia-crown-rot", + "asparagus-watery-soft-rot", + "asparagus-zopfia-root-rot", + "asparagus-", + "tea-anthracnoseref-namepandey-et-al-2021", + "tea-armillaria-root-rot", + "tea-birds-eye-spot", + "tea-black-blight", + "tea-black-root-rot", + "tea-black-rot", + "tea-blister-blight", + "tea-botryodiplodia-root-rot", + "tea-brown-blightref-nameidguideref-namepandey-et-al-2021", + "tea-brown-root-rot", + "tea-brown-spot", + "tea-brown-zonate-leaf-blight", + "tea-bud-blight", + "tea-charcoal-stump-rot", + "tea-collar-and-branch-canker", + "tea-collar-rot", + "tea-copper-blight", + "tea-damping-off", + "tea-dieback", + "tea-gray-blightref-nameidguideref-namepandey-et-al-2021", + "tea-gray-mold", + "tea-gray-spot", + "tea-horse-hair-blightref-nameidguide", + "tea-leaf-spot", + "tea-leaf-scab", + "tea-macrophoma-stem-cankerref-namepandey-et-al-2021", + "tea-net-blister-blight", + "tea-pale-brown-root-rot", + "tea-phloem-necrosis", + "tea-phyllosticta-leaf-spot", + "tea-pink-disease", + "tea-poria-root-rot-and-stem-canker", + "tea-purple-root-rot", + "tea-red-leaf-spot", + "tea-red-root-rot", + "tea-red-rust-alga", + "tea-rim-blight", + "tea-root-rot", + "tea-rough-bark", + "tea-sclerotial-blight", + "tea-shoot-withering", + "tea-stump-rot", + "tea-tarry-root-rot", + "tea-thorny-stem-blight", + "tea-thread-blight", + "tea-twig-blight", + "tea-twig-dieback-stem-cankerref-nameidguideref-namepandey-et-al-2021", + "tea-velvet-blight", + "tea-violet-root-rot", + "tea-white-root-rot", + "tea-white-scab", + "tea-wood-rot", + "tea-xylaria-root-rot", + "tomato-blossom-end-rot", + "tomato-herbicide-injury", + "potato-blossom-end-rot", + "potato-herbicide-injury", + "bell-pepper-blossom-end-rot", + "bell-pepper-herbicide-injury", + "chili-pepper-blossom-end-rot", + "chili-pepper-herbicide-injury", + "eggplant-herbicide-injury", + "tobacco-blossom-end-rot", + "tobacco-herbicide-injury", + "tomatillo-blossom-end-rot", + "tomatillo-herbicide-injury", + "petunia-blossom-end-rot", + "petunia-herbicide-injury", + "gooseberry-blossom-end-rot", + "gooseberry-herbicide-injury", + "cucumber-herbicide-injury", + "zucchini-blossom-end-rot", + "zucchini-herbicide-injury", + "summer-squash-blossom-end-rot", + "summer-squash-herbicide-injury", + "winter-squash-blossom-end-rot", + "winter-squash-herbicide-injury", + "pumpkin-blossom-end-rot", + "pumpkin-herbicide-injury", + "watermelon-herbicide-injury", + "cantaloupe-blossom-end-rot", + "cantaloupe-herbicide-injury", + "honeydew-blossom-end-rot", + "honeydew-herbicide-injury", + "bitter-melon-blossom-end-rot", + "bitter-melon-herbicide-injury", + "chayote-blossom-end-rot", + "chayote-herbicide-injury", + "acorn-squash-blossom-end-rot", + "acorn-squash-herbicide-injury", + "butternut-squash-blossom-end-rot", + "butternut-squash-herbicide-injury", + "calabash-blossom-end-rot", + "calabash-herbicide-injury", + "luffa-blossom-end-rot", + "luffa-herbicide-injury", + "apple-blossom-end-rot", + "apple-herbicide-injury", + "pear-blossom-end-rot", + "pear-herbicide-injury", + "peach-blossom-end-rot", + "peach-herbicide-injury", + "cherry-blossom-end-rot", + "cherry-herbicide-injury", + "apricot-blossom-end-rot", + "apricot-herbicide-injury", + "plum-blossom-end-rot", + "plum-herbicide-injury", + "almond-blossom-end-rot", + "almond-herbicide-injury", + "strawberry-blossom-end-rot", + "strawberry-herbicide-injury", + "raspberry-blossom-end-rot", + "raspberry-herbicide-injury", + "blackberry-blossom-end-rot", + "blackberry-herbicide-injury", + "blueberry-blossom-end-rot", + "blueberry-herbicide-injury", + "cranberry-blossom-end-rot", + "cranberry-herbicide-injury", + "rose-blossom-end-rot", + "rose-herbicide-injury", + "hawthorn-blossom-end-rot", + "hawthorn-herbicide-injury", + "quince-blossom-end-rot", + "quince-herbicide-injury", + "cabbage-blossom-end-rot", + "cabbage-herbicide-injury", + "broccoli-blossom-end-rot", + "broccoli-herbicide-injury", + "cauliflower-blossom-end-rot", + "cauliflower-herbicide-injury", + "brussels-sprouts-blossom-end-rot", + "brussels-sprouts-herbicide-injury", + "kale-blossom-end-rot", + "kale-herbicide-injury", + "bok-choy-blossom-end-rot", + "bok-choy-herbicide-injury", + "radish-blossom-end-rot", + "radish-herbicide-injury", + "turnip-blossom-end-rot", + "turnip-herbicide-injury", + "arugula-blossom-end-rot", + "arugula-herbicide-injury", + "collard-greens-blossom-end-rot", + "collard-greens-herbicide-injury", + "mustard-greens-blossom-end-rot", + "mustard-greens-herbicide-injury", + "horseradish-blossom-end-rot", + "horseradish-herbicide-injury", + "wasabi-blossom-end-rot", + "wasabi-herbicide-injury", + "green-bean-blossom-end-rot", + "green-bean-herbicide-injury", + "soybean-blossom-end-rot", + "soybean-herbicide-injury", + "peanut-blossom-end-rot", + "peanut-herbicide-injury", + "chickpea-blossom-end-rot", + "chickpea-herbicide-injury", + "lentil-blossom-end-rot", + "lentil-herbicide-injury", + "faba-bean-blossom-end-rot", + "faba-bean-herbicide-injury", + "cowpea-blossom-end-rot", + "cowpea-herbicide-injury", + "pigeon-pea-blossom-end-rot", + "pigeon-pea-herbicide-injury", + "alfalfa-blossom-end-rot", + "alfalfa-herbicide-injury", + "clover-blossom-end-rot", + "clover-herbicide-injury", + "peas-blossom-end-rot", + "peas-herbicide-injury", + "lupine-blossom-end-rot", + "lupine-herbicide-injury", + "wisteria-blossom-end-rot", + "wisteria-herbicide-injury", + "robinia-blossom-end-rot", + "robinia-herbicide-injury", + "corn-blossom-end-rot", + "corn-herbicide-injury", + "wheat-blossom-end-rot", + "wheat-herbicide-injury", + "rice-blossom-end-rot", + "rice-herbicide-injury", + "barley-blossom-end-rot", + "barley-herbicide-injury", + "oats-blossom-end-rot", + "oats-herbicide-injury", + "sorghum-blossom-end-rot", + "sorghum-herbicide-injury", + "sugarcane-blossom-end-rot", + "sugarcane-herbicide-injury", + "bamboo-blossom-end-rot", + "bamboo-herbicide-injury", + "turfgrass-blossom-end-rot", + "turfgrass-herbicide-injury", + "millet-blossom-end-rot", + "millet-herbicide-injury", + "rye-blossom-end-rot", + "rye-herbicide-injury", + "sunflower-blossom-end-rot", + "sunflower-herbicide-injury", + "lettuce-blossom-end-rot", + "lettuce-herbicide-injury", + "artichoke-blossom-end-rot", + "artichoke-herbicide-injury", + "chicory-blossom-end-rot", + "chicory-herbicide-injury", + "endive-blossom-end-rot", + "endive-herbicide-injury", + "daisy-blossom-end-rot", + "daisy-herbicide-injury", + "marigold-blossom-end-rot", + "marigold-herbicide-injury", + "zinnia-blossom-end-rot", + "zinnia-herbicide-injury", + "chrysanthemum-blossom-end-rot", + "chrysanthemum-herbicide-injury", + "dahlia-blossom-end-rot", + "dahlia-herbicide-injury", + "calendula-blossom-end-rot", + "calendula-herbicide-injury", + "echinacea-blossom-end-rot", + "echinacea-herbicide-injury", + "yarrow-blossom-end-rot", + "yarrow-herbicide-injury", + "tarragon-blossom-end-rot", + "tarragon-herbicide-injury", + "stevia-blossom-end-rot", + "stevia-herbicide-injury", + "basil-blossom-end-rot", + "basil-herbicide-injury", + "mint-blossom-end-rot", + "mint-herbicide-injury", + "lavender-blossom-end-rot", + "lavender-herbicide-injury", + "rosemary-blossom-end-rot", + "rosemary-herbicide-injury", + "thyme-blossom-end-rot", + "thyme-herbicide-injury", + "oregano-blossom-end-rot", + "oregano-herbicide-injury", + "sage-blossom-end-rot", + "sage-herbicide-injury", + "lemon-balm-blossom-end-rot", + "lemon-balm-herbicide-injury", + "catnip-blossom-end-rot", + "catnip-herbicide-injury", + "coleus-blossom-end-rot", + "coleus-herbicide-injury", + "carrot-blossom-end-rot", + "carrot-herbicide-injury", + "celery-blossom-end-rot", + "celery-herbicide-injury", + "parsley-blossom-end-rot", + "parsley-herbicide-injury", + "cilantro-blossom-end-rot", + "cilantro-herbicide-injury", + "dill-blossom-end-rot", + "dill-herbicide-injury", + "fennel-blossom-end-rot", + "fennel-herbicide-injury", + "parsnip-blossom-end-rot", + "parsnip-herbicide-injury", + "cumin-blossom-end-rot", + "cumin-herbicide-injury", + "onion-blossom-end-rot", + "onion-herbicide-injury", + "garlic-blossom-end-rot", + "garlic-herbicide-injury", + "leek-blossom-end-rot", + "leek-herbicide-injury", + "shallot-anthracnose", + "shallot-bacterial-leaf-spot", + "shallot-blossom-end-rot", + "shallot-herbicide-injury", + "chive-powdery-mildew", + "chive-anthracnose", + "chive-bacterial-leaf-spot", + "chive-blossom-end-rot", + "chive-herbicide-injury", + "monstera-bacterial-leaf-spot-aroids", + "monstera-powdery-mildew", + "monstera-anthracnose", + "monstera-bacterial-leaf-spot", + "monstera-blossom-end-rot", + "monstera-herbicide-injury", + "pothos-blossom-end-rot", + "pothos-herbicide-injury", + "peace-lily-blossom-end-rot", + "peace-lily-herbicide-injury", + "philodendron-blossom-end-rot", + "philodendron-herbicide-injury", + "anthurium-blossom-end-rot", + "anthurium-herbicide-injury", + "alocasia-blossom-end-rot", + "alocasia-herbicide-injury", + "caladium-blossom-end-rot", + "caladium-herbicide-injury", + "aglaonema-blossom-end-rot", + "aglaonema-herbicide-injury", + "dieffenbachia-blossom-end-rot", + "dieffenbachia-herbicide-injury", + "spathiphyllum-blossom-end-rot", + "spathiphyllum-herbicide-injury", + "asparagus-blossom-end-rot", + "asparagus-herbicide-injury", + "snake-plant-blossom-end-rot", + "snake-plant-herbicide-injury", + "yucca-blossom-end-rot", + "yucca-herbicide-injury", + "dracaena-blossom-end-rot", + "dracaena-herbicide-injury", + "lily-of-the-valley-blossom-end-rot", + "lily-of-the-valley-herbicide-injury", + "hosta-blossom-end-rot", + "hosta-herbicide-injury", + "orchid-phalaenopsis-blossom-end-rot", + "orchid-phalaenopsis-herbicide-injury", + "orchid-cattleya-blossom-end-rot", + "orchid-cattleya-herbicide-injury", + "orchid-dendrobium-blossom-end-rot", + "orchid-dendrobium-herbicide-injury", + "orchid-oncidium-blossom-end-rot", + "orchid-oncidium-herbicide-injury", + "vanilla-blossom-end-rot", + "vanilla-herbicide-injury", + "prickly-pear-blossom-end-rot", + "prickly-pear-herbicide-injury", + "barrel-cactus-blossom-end-rot", + "barrel-cactus-herbicide-injury", + "christmas-cactus-blossom-end-rot", + "christmas-cactus-herbicide-injury", + "saguaro-blossom-end-rot", + "saguaro-herbicide-injury", + "aloe-vera-blossom-end-rot", + "aloe-vera-herbicide-injury", + "agave-blossom-end-rot", + "agave-herbicide-injury", + "echeveria-blossom-end-rot", + "echeveria-herbicide-injury", + "jade-plant-blossom-end-rot", + "jade-plant-herbicide-injury", + "sedum-blossom-end-rot", + "sedum-herbicide-injury", + "haworthia-blossom-end-rot", + "haworthia-herbicide-injury", + "poinsettia-blossom-end-rot", + "poinsettia-herbicide-injury", + "cassava-blossom-end-rot", + "cassava-herbicide-injury", + "castor-bean-blossom-end-rot", + "castor-bean-herbicide-injury", + "crown-of-thorns-blossom-end-rot", + "crown-of-thorns-herbicide-injury", + "orange-blossom-end-rot", + "orange-herbicide-injury", + "lemon-blossom-end-rot", + "lemon-herbicide-injury", + "lime-blossom-end-rot", + "lime-herbicide-injury", + "grapefruit-blossom-end-rot", + "grapefruit-herbicide-injury", + "mandarin-blossom-end-rot", + "mandarin-herbicide-injury", + "kumquat-blossom-end-rot", + "kumquat-herbicide-injury", + "grape-blossom-end-rot", + "grape-herbicide-injury", + "muscadine-blossom-end-rot", + "muscadine-herbicide-injury", + "banana-blossom-end-rot", + "banana-herbicide-injury", + "plantain-blossom-end-rot", + "plantain-herbicide-injury", + "bird-of-paradise-blossom-end-rot", + "bird-of-paradise-herbicide-injury", + "avocado-blossom-end-rot", + "avocado-herbicide-injury", + "cinnamon-blossom-end-rot", + "cinnamon-herbicide-injury", + "bay-laurel-blossom-end-rot", + "bay-laurel-herbicide-injury", + "cocoa-blossom-end-rot", + "cocoa-herbicide-injury", + "cotton-blossom-end-rot", + "cotton-herbicide-injury", + "okra-blossom-end-rot", + "okra-herbicide-injury", + "hibiscus-blossom-end-rot", + "hibiscus-herbicide-injury", + "hollyhock-bacterial-leaf-spot", + "hollyhock-blossom-end-rot", + "hollyhock-herbicide-injury", + "baobab-powdery-mildew", + "baobab-anthracnose", + "baobab-bacterial-leaf-spot", + "baobab-blossom-end-rot", + "baobab-herbicide-injury", + "durian-powdery-mildew", + "durian-anthracnose", + "durian-bacterial-leaf-spot", + "durian-blossom-end-rot", + "durian-herbicide-injury", + "coconut-blossom-end-rot", + "coconut-herbicide-injury", + "oil-palm-blossom-end-rot", + "oil-palm-herbicide-injury", + "date-palm-blossom-end-rot", + "date-palm-herbicide-injury", + "palm-areca-blossom-end-rot", + "palm-areca-herbicide-injury", + "palm-parlor-blossom-end-rot", + "palm-parlor-herbicide-injury", + "palm-kentia-blossom-end-rot", + "palm-kentia-herbicide-injury", + "mango-blossom-end-rot", + "mango-herbicide-injury", + "cashew-blossom-end-rot", + "cashew-herbicide-injury", + "pistachio-blossom-end-rot", + "pistachio-herbicide-injury", + "poison-ivy-blossom-end-rot", + "poison-ivy-herbicide-injury", + "coffee-blossom-end-rot", + "coffee-herbicide-injury", + "gardenia-blossom-end-rot", + "gardenia-herbicide-injury", + "tea-blossom-end-rot", + "tea-herbicide-injury", + "camellia-blossom-end-rot", + "camellia-herbicide-injury", + "pine-blossom-end-rot", + "pine-herbicide-injury", + "spruce-blossom-end-rot", + "spruce-herbicide-injury", + "fir-blossom-end-rot", + "fir-herbicide-injury", + "cedar-blossom-end-rot", + "cedar-herbicide-injury", + "juniper-blossom-end-rot", + "juniper-herbicide-injury", + "cypress-blossom-end-rot", + "cypress-herbicide-injury", + "arborvitae-blossom-end-rot", + "arborvitae-herbicide-injury", + "oak-blossom-end-rot", + "oak-herbicide-injury", + "beech-blossom-end-rot", + "beech-herbicide-injury", + "chestnut-blossom-end-rot", + "chestnut-herbicide-injury", + "fiddle-leaf-fig-blossom-end-rot", + "fiddle-leaf-fig-herbicide-injury", + "rubber-tree-blossom-end-rot", + "rubber-tree-herbicide-injury", + "weeping-fig-blossom-end-rot", + "weeping-fig-herbicide-injury", + "fig-blossom-end-rot", + "fig-herbicide-injury", + "mulberry-blossom-end-rot", + "mulberry-herbicide-injury", + "breadfruit-blossom-end-rot", + "breadfruit-herbicide-injury", + "eucalyptus-blossom-end-rot", + "eucalyptus-herbicide-injury", + "guava-blossom-end-rot", + "guava-herbicide-injury", + "clove-blossom-end-rot", + "clove-herbicide-injury", + "pineapple-blossom-end-rot", + "pineapple-herbicide-injury", + "bromeliad-blossom-end-rot", + "bromeliad-herbicide-injury", + "spanish-moss-blossom-end-rot", + "spanish-moss-herbicide-injury", + "sweet-potato-blossom-end-rot", + "sweet-potato-herbicide-injury", + "morning-glory-blossom-end-rot", + "morning-glory-herbicide-injury", + "spinach-blossom-end-rot", + "spinach-herbicide-injury", + "swiss-chard-blossom-end-rot", + "swiss-chard-herbicide-injury", + "beet-blossom-end-rot", + "beet-herbicide-injury", + "quinoa-blossom-end-rot", + "quinoa-herbicide-injury", + "amaranth-blossom-end-rot", + "amaranth-herbicide-injury", + "rhubarb-blossom-end-rot", + "rhubarb-herbicide-injury", + "buckwheat-blossom-end-rot", + "buckwheat-herbicide-injury", + "papaya-blossom-end-rot", + "papaya-herbicide-injury", + "olive-blossom-end-rot", + "olive-herbicide-injury", + "jasmine-blossom-end-rot", + "jasmine-herbicide-injury", + "lilac-blossom-end-rot", + "lilac-herbicide-injury", + "ash-blossom-end-rot", + "ash-herbicide-injury", + "hops-blossom-end-rot", + "hops-herbicide-injury", + "hemp-rust", + "hemp-bacterial-leaf-spot", + "hemp-blossom-end-rot", + "hemp-herbicide-injury", + "fern-boston-powdery-mildew", + "fern-boston-anthracnose", + "fern-boston-rust", + "fern-boston-bacterial-leaf-spot", + "fern-boston-blossom-end-rot", + "fern-boston-herbicide-injury", + "fern-maidenhair-powdery-mildew", + "fern-maidenhair-anthracnose", + "fern-maidenhair-rust", + "fern-maidenhair-bacterial-leaf-spot", + "fern-maidenhair-blossom-end-rot", + "fern-maidenhair-herbicide-injury", + "spider-plant-powdery-mildew", + "spider-plant-anthracnose", + "spider-plant-rust", + "spider-plant-bacterial-leaf-spot", + "spider-plant-blossom-end-rot", + "spider-plant-herbicide-injury", + "zz-plant-bacterial-leaf-spot-aroids", + "zz-plant-powdery-mildew", + "zz-plant-anthracnose", + "zz-plant-rust", + "zz-plant-bacterial-leaf-spot", + "zz-plant-blossom-end-rot", + "zz-plant-herbicide-injury", + "prayer-plant-powdery-mildew", + "prayer-plant-anthracnose", + "prayer-plant-rust", + "prayer-plant-bacterial-leaf-spot", + "prayer-plant-blossom-end-rot", + "prayer-plant-herbicide-injury", + "calathea-powdery-mildew", + "calathea-anthracnose", + "calathea-rust", + "calathea-bacterial-leaf-spot", + "calathea-blossom-end-rot", + "calathea-herbicide-injury", + "pilea-powdery-mildew", + "pilea-anthracnose", + "pilea-rust", + "pilea-bacterial-leaf-spot", + "pilea-blossom-end-rot", + "pilea-herbicide-injury", + "tradescantia-powdery-mildew", + "tradescantia-anthracnose", + "tradescantia-rust", + "tradescantia-bacterial-leaf-spot", + "tradescantia-blossom-end-rot", + "tradescantia-herbicide-injury", + "succulent-echeveria-powdery-mildew", + "succulent-echeveria-anthracnose", + "succulent-echeveria-rust", + "succulent-echeveria-bacterial-leaf-spot", + "succulent-echeveria-blossom-end-rot", + "succulent-echeveria-herbicide-injury", + "money-tree-powdery-mildew", + "money-tree-anthracnose", + "money-tree-rust", + "money-tree-bacterial-leaf-spot", + "money-tree-blossom-end-rot", + "money-tree-herbicide-injury", + "palm-cat-powdery-mildew", + "palm-cat-anthracnose", + "palm-cat-rust", + "palm-cat-bacterial-leaf-spot", + "palm-cat-blossom-end-rot", + "palm-cat-herbicide-injury", + "ficus-altissima-powdery-mildew", + "ficus-altissima-anthracnose", + "ficus-altissima-rust", + "ficus-altissima-bacterial-leaf-spot", + "ficus-altissima-blossom-end-rot", + "ficus-altissima-herbicide-injury", + "string-of-pearls-aster-yellows", + "string-of-pearls-powdery-mildew", + "string-of-pearls-anthracnose", + "string-of-pearls-rust", + "string-of-pearls-bacterial-leaf-spot", + "string-of-pearls-blossom-end-rot", + "string-of-pearls-herbicide-injury", + "burros-tail-powdery-mildew", + "burros-tail-anthracnose", + "burros-tail-rust", + "burros-tail-bacterial-leaf-spot", + "burros-tail-blossom-end-rot", + "burros-tail-herbicide-injury", + "snake-plant-masoniana-powdery-mildew", + "snake-plant-masoniana-anthracnose", + "snake-plant-masoniana-rust", + "snake-plant-masoniana-bacterial-leaf-spot", + "snake-plant-masoniana-blossom-end-rot", + "snake-plant-masoniana-herbicide-injury", + "passion-fruit-powdery-mildew", + "passion-fruit-anthracnose", + "passion-fruit-rust", + "passion-fruit-bacterial-leaf-spot", + "passion-fruit-blossom-end-rot", + "passion-fruit-herbicide-injury", + "kiwi-powdery-mildew", + "kiwi-anthracnose", + "kiwi-rust", + "kiwi-bacterial-leaf-spot", + "kiwi-blossom-end-rot", + "kiwi-herbicide-injury", + "lychee-powdery-mildew", + "lychee-anthracnose", + "lychee-rust", + "lychee-bacterial-leaf-spot", + "lychee-blossom-end-rot", + "lychee-herbicide-injury", + "rambutan-powdery-mildew", + "rambutan-anthracnose", + "rambutan-rust", + "rambutan-bacterial-leaf-spot", + "rambutan-blossom-end-rot", + "rambutan-herbicide-injury", + "jackfruit-powdery-mildew", + "jackfruit-anthracnose", + "jackfruit-rust", + "jackfruit-bacterial-leaf-spot", + "jackfruit-blossom-end-rot", + "jackfruit-herbicide-injury", + "dragon-fruit-powdery-mildew", + "dragon-fruit-anthracnose", + "dragon-fruit-rust", + "dragon-fruit-bacterial-leaf-spot", + "dragon-fruit-blossom-end-rot", + "dragon-fruit-herbicide-injury", + "pomegranate-powdery-mildew", + "pomegranate-anthracnose", + "pomegranate-rust", + "pomegranate-bacterial-leaf-spot", + "pomegranate-blossom-end-rot", + "pomegranate-herbicide-injury", + "persimmon-powdery-mildew", + "persimmon-anthracnose", + "persimmon-rust", + "persimmon-bacterial-leaf-spot", + "persimmon-blossom-end-rot", + "persimmon-herbicide-injury", + "tulip-powdery-mildew", + "tulip-anthracnose", + "tulip-rust", + "tulip-bacterial-leaf-spot", + "tulip-blossom-end-rot", + "tulip-herbicide-injury", + "daffodil-powdery-mildew", + "daffodil-anthracnose", + "daffodil-rust", + "daffodil-bacterial-leaf-spot", + "daffodil-blossom-end-rot", + "daffodil-herbicide-injury", + "iris-powdery-mildew", + "iris-anthracnose", + "iris-rust", + "iris-bacterial-leaf-spot", + "iris-blossom-end-rot", + "iris-herbicide-injury", + "lily-powdery-mildew", + "lily-anthracnose", + "lily-rust", + "lily-bacterial-leaf-spot", + "lily-blossom-end-rot", + "lily-herbicide-injury", + "peony-powdery-mildew", + "peony-anthracnose", + "peony-rust", + "peony-bacterial-leaf-spot", + "peony-blossom-end-rot", + "peony-herbicide-injury", + "hydrangea-powdery-mildew", + "hydrangea-anthracnose", + "hydrangea-rust", + "hydrangea-bacterial-leaf-spot", + "hydrangea-blossom-end-rot", + "hydrangea-herbicide-injury", + "rhododendron-mummy-berry-blueberry", + "rhododendron-powdery-mildew", + "rhododendron-anthracnose", + "rhododendron-rust", + "rhododendron-bacterial-leaf-spot", + "rhododendron-blossom-end-rot", + "rhododendron-herbicide-injury", + "azalea-mummy-berry-blueberry", + "azalea-powdery-mildew", + "azalea-anthracnose", + "azalea-rust", + "azalea-bacterial-leaf-spot", + "azalea-blossom-end-rot", + "azalea-herbicide-injury", + "magnolia-powdery-mildew", + "magnolia-anthracnose", + "magnolia-rust", + "magnolia-bacterial-leaf-spot", + "magnolia-blossom-end-rot", + "magnolia-herbicide-injury", + "dogwood-powdery-mildew", + "dogwood-anthracnose", + "dogwood-rust", + "dogwood-bacterial-leaf-spot", + "dogwood-blossom-end-rot", + "dogwood-herbicide-injury", + "maple-powdery-mildew", + "maple-anthracnose", + "maple-rust", + "maple-bacterial-leaf-spot", + "maple-blossom-end-rot", + "maple-herbicide-injury", + "birch-powdery-mildew", + "birch-anthracnose", + "birch-rust", + "birch-bacterial-leaf-spot", + "birch-blossom-end-rot", + "birch-herbicide-injury", + "elm-powdery-mildew", + "elm-anthracnose", + "elm-rust", + "elm-bacterial-leaf-spot", + "elm-blossom-end-rot", + "elm-herbicide-injury", + "willow-powdery-mildew", + "willow-anthracnose", + "willow-rust", + "willow-bacterial-leaf-spot", + "willow-blossom-end-rot", + "willow-herbicide-injury", + "poplar-powdery-mildew", + "poplar-anthracnose", + "poplar-rust", + "poplar-bacterial-leaf-spot", + "poplar-blossom-end-rot", + "poplar-herbicide-injury", + "sycamore-powdery-mildew", + "sycamore-anthracnose", + "sycamore-rust", + "sycamore-bacterial-leaf-spot", + "sycamore-blossom-end-rot", + "sycamore-herbicide-injury", + "hickory-powdery-mildew", + "hickory-anthracnose", + "hickory-rust", + "hickory-bacterial-leaf-spot", + "hickory-blossom-end-rot", + "hickory-herbicide-injury", + "pecan-powdery-mildew", + "pecan-anthracnose", + "pecan-rust", + "pecan-bacterial-leaf-spot", + "pecan-blossom-end-rot", + "pecan-herbicide-injury", + "walnut-powdery-mildew", + "walnut-anthracnose", + "walnut-rust", + "walnut-bacterial-leaf-spot", + "walnut-blossom-end-rot", + "walnut-herbicide-injury", + "tomato-lesion-nematode", + "bell-pepper-lesion-nematode", + "chili-pepper-lesion-nematode", + "eggplant-lesion-nematode", + "tobacco-lesion-nematode", + "tomatillo-lesion-nematode", + "petunia-lesion-nematode", + "gooseberry-lesion-nematode", + "cucumber-lesion-nematode", + "zucchini-lesion-nematode", + "summer-squash-lesion-nematode", + "winter-squash-lesion-nematode", + "pumpkin-lesion-nematode", + "watermelon-lesion-nematode", + "cantaloupe-lesion-nematode", + "honeydew-lesion-nematode", + "bitter-melon-lesion-nematode" ], "currentKeyIndex": 0, "callsThisKey": 1068, diff --git a/apps/web/scripts/.ddg-progress.json b/apps/web/scripts/.ddg-progress.json index 28ade8f..a1afd97 100644 --- a/apps/web/scripts/.ddg-progress.json +++ b/apps/web/scripts/.ddg-progress.json @@ -649,7 +649,1157 @@ "philodendron-monstera-root-knot-nematode", "philodendron-monstera-canker-stembranch", "philodendron-monstera-bacterial-soft-rot", - "philodendron-monstera-downy-mildew-generic" + "philodendron-monstera-downy-mildew-generic", + "philodendron-monstera-viral-leaf-curl", + "philodendron-monstera-wood-rot-decay", + "pothos-marble-queen-root-rot-aroidsoverwatering", + "pothos-marble-queen-root-rot-pythiumphytophthora", + "pothos-marble-queen-damping-off", + "pothos-marble-queen-gray-mold-botrytis-blight", + "pothos-marble-queen-mosaic-virus", + "pothos-marble-queen-wilt-fusarium-or-verticillium", + "pothos-marble-queen-root-knot-nematode", + "pothos-marble-queen-canker-stembranch", + "pothos-marble-queen-bacterial-soft-rot", + "pothos-marble-queen-downy-mildew-generic", + "pothos-marble-queen-viral-leaf-curl", + "pothos-marble-queen-wood-rot-decay", + "peace-lily-sensation-root-rot-aroidsoverwatering", + "peace-lily-sensation-root-rot-pythiumphytophthora", + "peace-lily-sensation-damping-off", + "peace-lily-sensation-gray-mold-botrytis-blight", + "peace-lily-sensation-mosaic-virus", + "peace-lily-sensation-wilt-fusarium-or-verticillium", + "peace-lily-sensation-root-knot-nematode", + "peace-lily-sensation-canker-stembranch", + "peace-lily-sensation-bacterial-soft-rot", + "peace-lily-sensation-downy-mildew-generic", + "peace-lily-sensation-viral-leaf-curl", + "peace-lily-sensation-wood-rot-decay", + "phalaenopsis-orchid-root-rot-pythiumphytophthora", + "phalaenopsis-orchid-damping-off", + "phalaenopsis-orchid-gray-mold-botrytis-blight", + "phalaenopsis-orchid-mosaic-virus", + "phalaenopsis-orchid-wilt-fusarium-or-verticillium", + "phalaenopsis-orchid-root-knot-nematode", + "phalaenopsis-orchid-canker-stembranch", + "phalaenopsis-orchid-bacterial-soft-rot", + "phalaenopsis-orchid-downy-mildew-generic", + "phalaenopsis-orchid-viral-leaf-curl", + "phalaenopsis-orchid-wood-rot-decay", + "cattleya-orchid-root-rot-pythiumphytophthora", + "cattleya-orchid-damping-off", + "cattleya-orchid-gray-mold-botrytis-blight", + "cattleya-orchid-mosaic-virus", + "cattleya-orchid-wilt-fusarium-or-verticillium", + "cattleya-orchid-root-knot-nematode", + "cattleya-orchid-canker-stembranch", + "cattleya-orchid-bacterial-soft-rot", + "cattleya-orchid-downy-mildew-generic", + "cattleya-orchid-viral-leaf-curl", + "cattleya-orchid-wood-rot-decay", + "dendrobium-orchid-root-rot-pythiumphytophthora", + "dendrobium-orchid-damping-off", + "dendrobium-orchid-gray-mold-botrytis-blight", + "dendrobium-orchid-mosaic-virus", + "dendrobium-orchid-wilt-fusarium-or-verticillium", + "dendrobium-orchid-root-knot-nematode", + "dendrobium-orchid-canker-stembranch", + "dendrobium-orchid-bacterial-soft-rot", + "dendrobium-orchid-downy-mildew-generic", + "dendrobium-orchid-viral-leaf-curl", + "dendrobium-orchid-wood-rot-decay", + "oncidium-orchid-root-rot-pythiumphytophthora", + "oncidium-orchid-damping-off", + "oncidium-orchid-gray-mold-botrytis-blight", + "oncidium-orchid-mosaic-virus", + "oncidium-orchid-wilt-fusarium-or-verticillium", + "oncidium-orchid-root-knot-nematode", + "oncidium-orchid-canker-stembranch", + "oncidium-orchid-bacterial-soft-rot", + "oncidium-orchid-downy-mildew-generic", + "oncidium-orchid-viral-leaf-curl", + "oncidium-orchid-wood-rot-decay", + "begonia-root-rot-pythiumphytophthora", + "begonia-damping-off", + "begonia-gray-mold-botrytis-blight", + "begonia-mosaic-virus", + "begonia-wilt-fusarium-or-verticillium", + "begonia-root-knot-nematode", + "begonia-canker-stembranch", + "begonia-bacterial-soft-rot", + "begonia-downy-mildew-generic", + "begonia-viral-leaf-curl", + "begonia-wood-rot-decay", + "impatiens-root-rot-pythiumphytophthora", + "impatiens-damping-off", + "impatiens-gray-mold-botrytis-blight", + "impatiens-mosaic-virus", + "impatiens-wilt-fusarium-or-verticillium", + "impatiens-root-knot-nematode", + "impatiens-canker-stembranch", + "impatiens-bacterial-soft-rot", + "impatiens-downy-mildew-generic", + "impatiens-viral-leaf-curl", + "impatiens-wood-rot-decay", + "geranium-root-rot-pythiumphytophthora", + "geranium-damping-off", + "geranium-gray-mold-botrytis-blight", + "geranium-mosaic-virus", + "geranium-wilt-fusarium-or-verticillium", + "geranium-root-knot-nematode", + "geranium-canker-stembranch", + "geranium-bacterial-soft-rot", + "geranium-downy-mildew-generic", + "geranium-viral-leaf-curl", + "geranium-wood-rot-decay", + "cyclamen-root-rot-pythiumphytophthora", + "cyclamen-damping-off", + "cyclamen-gray-mold-botrytis-blight", + "cyclamen-mosaic-virus", + "cyclamen-wilt-fusarium-or-verticillium", + "cyclamen-root-knot-nematode", + "cyclamen-canker-stembranch", + "cyclamen-bacterial-soft-rot", + "cyclamen-downy-mildew-generic", + "cyclamen-viral-leaf-curl", + "cyclamen-wood-rot-decay", + "african-violet-root-rot-pythiumphytophthora", + "african-violet-damping-off", + "african-violet-gray-mold-botrytis-blight", + "african-violet-mosaic-virus", + "african-violet-wilt-fusarium-or-verticillium", + "african-violet-root-knot-nematode", + "african-violet-canker-stembranch", + "african-violet-bacterial-soft-rot", + "african-violet-downy-mildew-generic", + "african-violet-viral-leaf-curl", + "african-violet-wood-rot-decay", + "gloxinia-root-rot-pythiumphytophthora", + "gloxinia-damping-off", + "gloxinia-gray-mold-botrytis-blight", + "gloxinia-mosaic-virus", + "gloxinia-wilt-fusarium-or-verticillium", + "gloxinia-root-knot-nematode", + "gloxinia-canker-stembranch", + "gloxinia-bacterial-soft-rot", + "gloxinia-downy-mildew-generic", + "gloxinia-viral-leaf-curl", + "gloxinia-wood-rot-decay", + "cucumber-horned-downy-mildew-cucurbits", + "cucumber-horned-gummy-stem-blight", + "cucumber-horned-root-rot-pythiumphytophthora", + "cucumber-horned-damping-off", + "cucumber-horned-gray-mold-botrytis-blight", + "cucumber-horned-mosaic-virus", + "cucumber-horned-wilt-fusarium-or-verticillium", + "cucumber-horned-root-knot-nematode", + "cucumber-horned-canker-stembranch", + "cucumber-horned-bacterial-soft-rot", + "cucumber-horned-downy-mildew-generic", + "cucumber-horned-viral-leaf-curl", + "cucumber-horned-wood-rot-decay", + "sweet-potato-leaf-root-rot-pythiumphytophthora", + "sweet-potato-leaf-damping-off", + "sweet-potato-leaf-gray-mold-botrytis-blight", + "sweet-potato-leaf-mosaic-virus", + "sweet-potato-leaf-wilt-fusarium-or-verticillium", + "sweet-potato-leaf-root-knot-nematode", + "sweet-potato-leaf-canker-stembranch", + "sweet-potato-leaf-bacterial-soft-rot", + "sweet-potato-leaf-downy-mildew-generic", + "sweet-potato-leaf-viral-leaf-curl", + "sweet-potato-leaf-wood-rot-decay", + "ivy-english-root-rot-pythiumphytophthora", + "ivy-english-damping-off", + "ivy-english-gray-mold-botrytis-blight", + "ivy-english-mosaic-virus", + "ivy-english-wilt-fusarium-or-verticillium", + "ivy-english-root-knot-nematode", + "ivy-english-canker-stembranch", + "ivy-english-bacterial-soft-rot", + "ivy-english-downy-mildew-generic", + "ivy-english-viral-leaf-curl", + "ivy-english-wood-rot-decay", + "ivy-swedish-downy-mildew-lamiaceaebasil", + "ivy-swedish-basil-fusarium-wilt", + "ivy-swedish-root-rot-pythiumphytophthora", + "ivy-swedish-damping-off", + "ivy-swedish-gray-mold-botrytis-blight", + "ivy-swedish-mosaic-virus", + "ivy-swedish-wilt-fusarium-or-verticillium", + "ivy-swedish-root-knot-nematode", + "ivy-swedish-canker-stembranch", + "ivy-swedish-bacterial-soft-rot", + "ivy-swedish-downy-mildew-generic", + "ivy-swedish-viral-leaf-curl", + "ivy-swedish-wood-rot-decay", + "banana-dwarf-root-rot-pythiumphytophthora", + "banana-dwarf-damping-off", + "banana-dwarf-gray-mold-botrytis-blight", + "banana-dwarf-mosaic-virus", + "banana-dwarf-wilt-fusarium-or-verticillium", + "banana-dwarf-root-knot-nematode", + "banana-dwarf-canker-stembranch", + "banana-dwarf-bacterial-soft-rot", + "banana-dwarf-downy-mildew-generic", + "banana-dwarf-viral-leaf-curl", + "banana-dwarf-wood-rot-decay", + "mimosa-white-mold-sclerotinia-rot", + "mimosa-charcoal-rot", + "mimosa-root-rot-pythiumphytophthora", + "mimosa-damping-off", + "mimosa-gray-mold-botrytis-blight", + "mimosa-mosaic-virus", + "mimosa-wilt-fusarium-or-verticillium", + "mimosa-root-knot-nematode", + "mimosa-canker-stembranch", + "mimosa-bacterial-soft-rot", + "mimosa-downy-mildew-generic", + "mimosa-viral-leaf-curl", + "mimosa-wood-rot-decay", + "kentucky-coffee-white-mold-sclerotinia-rot", + "kentucky-coffee-charcoal-rot", + "kentucky-coffee-root-rot-pythiumphytophthora", + "kentucky-coffee-damping-off", + "kentucky-coffee-gray-mold-botrytis-blight", + "kentucky-coffee-mosaic-virus", + "kentucky-coffee-wilt-fusarium-or-verticillium", + "kentucky-coffee-root-knot-nematode", + "kentucky-coffee-canker-stembranch", + "kentucky-coffee-bacterial-soft-rot", + "kentucky-coffee-downy-mildew-generic", + "kentucky-coffee-viral-leaf-curl", + "kentucky-coffee-wood-rot-decay", + "redbud-white-mold-sclerotinia-rot", + "redbud-charcoal-rot", + "redbud-root-rot-pythiumphytophthora", + "redbud-damping-off", + "redbud-gray-mold-botrytis-blight", + "redbud-mosaic-virus", + "redbud-wilt-fusarium-or-verticillium", + "redbud-root-knot-nematode", + "redbud-canker-stembranch", + "redbud-bacterial-soft-rot", + "redbud-downy-mildew-generic", + "redbud-viral-leaf-curl", + "redbud-wood-rot-decay", + "tulip-tree-root-rot-pythiumphytophthora", + "tulip-tree-damping-off", + "tulip-tree-gray-mold-botrytis-blight", + "tulip-tree-mosaic-virus", + "tulip-tree-wilt-fusarium-or-verticillium", + "tulip-tree-root-knot-nematode", + "tulip-tree-canker-stembranch", + "tulip-tree-bacterial-soft-rot", + "tulip-tree-downy-mildew-generic", + "tulip-tree-viral-leaf-curl", + "tulip-tree-wood-rot-decay", + "sweetgum-root-rot-pythiumphytophthora", + "sweetgum-damping-off", + "sweetgum-gray-mold-botrytis-blight", + "sweetgum-mosaic-virus", + "sweetgum-wilt-fusarium-or-verticillium", + "sweetgum-root-knot-nematode", + "sweetgum-canker-stembranch", + "sweetgum-bacterial-soft-rot", + "sweetgum-downy-mildew-generic", + "sweetgum-viral-leaf-curl", + "sweetgum-wood-rot-decay", + "crabapple-brown-rot-stone-fruit", + "crabapple-root-rot-pythiumphytophthora", + "crabapple-damping-off", + "crabapple-gray-mold-botrytis-blight", + "crabapple-mosaic-virus", + "crabapple-wilt-fusarium-or-verticillium", + "crabapple-root-knot-nematode", + "crabapple-canker-stembranch", + "crabapple-bacterial-soft-rot", + "crabapple-downy-mildew-generic", + "crabapple-viral-leaf-curl", + "crabapple-wood-rot-decay", + "serviceberry-brown-rot-stone-fruit", + "serviceberry-root-rot-pythiumphytophthora", + "serviceberry-damping-off", + "serviceberry-gray-mold-botrytis-blight", + "serviceberry-mosaic-virus", + "serviceberry-wilt-fusarium-or-verticillium", + "serviceberry-root-knot-nematode", + "serviceberry-canker-stembranch", + "serviceberry-bacterial-soft-rot", + "serviceberry-downy-mildew-generic", + "serviceberry-viral-leaf-curl", + "serviceberry-wood-rot-decay", + "chokecherry-brown-rot-stone-fruit", + "chokecherry-root-rot-pythiumphytophthora", + "chokecherry-damping-off", + "chokecherry-gray-mold-botrytis-blight", + "chokecherry-mosaic-virus", + "chokecherry-wilt-fusarium-or-verticillium", + "chokecherry-root-knot-nematode", + "chokecherry-canker-stembranch", + "chokecherry-bacterial-soft-rot", + "chokecherry-downy-mildew-generic", + "chokecherry-viral-leaf-curl", + "chokecherry-wood-rot-decay", + "buckeye-root-rot-pythiumphytophthora", + "buckeye-damping-off", + "buckeye-gray-mold-botrytis-blight", + "buckeye-mosaic-virus", + "buckeye-wilt-fusarium-or-verticillium", + "buckeye-root-knot-nematode", + "buckeye-canker-stembranch", + "buckeye-bacterial-soft-rot", + "buckeye-downy-mildew-generic", + "buckeye-viral-leaf-curl", + "buckeye-wood-rot-decay", + "linden-root-rot-pythiumphytophthora", + "linden-damping-off", + "linden-gray-mold-botrytis-blight", + "linden-mosaic-virus", + "linden-wilt-fusarium-or-verticillium", + "linden-root-knot-nematode", + "linden-canker-stembranch", + "linden-bacterial-soft-rot", + "linden-downy-mildew-generic", + "linden-viral-leaf-curl", + "linden-wood-rot-decay", + "ginkgo-root-rot-pythiumphytophthora", + "ginkgo-damping-off", + "ginkgo-gray-mold-botrytis-blight", + "ginkgo-mosaic-virus", + "ginkgo-wilt-fusarium-or-verticillium", + "ginkgo-root-knot-nematode", + "ginkgo-canker-stembranch", + "ginkgo-bacterial-soft-rot", + "ginkgo-downy-mildew-generic", + "ginkgo-viral-leaf-curl", + "ginkgo-wood-rot-decay", + "ficus-microcarpa-root-rot-pythiumphytophthora", + "ficus-microcarpa-damping-off", + "ficus-microcarpa-gray-mold-botrytis-blight", + "ficus-microcarpa-mosaic-virus", + "ficus-microcarpa-wilt-fusarium-or-verticillium", + "ficus-microcarpa-root-knot-nematode", + "ficus-microcarpa-canker-stembranch", + "ficus-microcarpa-bacterial-soft-rot", + "ficus-microcarpa-downy-mildew-generic", + "ficus-microcarpa-viral-leaf-curl", + "ficus-microcarpa-wood-rot-decay", + "schefflera-root-rot-pythiumphytophthora", + "schefflera-damping-off", + "schefflera-gray-mold-botrytis-blight", + "schefflera-mosaic-virus", + "schefflera-wilt-fusarium-or-verticillium", + "schefflera-root-knot-nematode", + "schefflera-canker-stembranch", + "schefflera-bacterial-soft-rot", + "schefflera-downy-mildew-generic", + "schefflera-viral-leaf-curl", + "schefflera-wood-rot-decay", + "maranta-root-rot-pythiumphytophthora", + "maranta-damping-off", + "maranta-gray-mold-botrytis-blight", + "maranta-mosaic-virus", + "maranta-wilt-fusarium-or-verticillium", + "maranta-root-knot-nematode", + "maranta-canker-stembranch", + "maranta-bacterial-soft-rot", + "maranta-downy-mildew-generic", + "maranta-viral-leaf-curl", + "maranta-wood-rot-decay", + "stromanthe-root-rot-pythiumphytophthora", + "stromanthe-damping-off", + "stromanthe-gray-mold-botrytis-blight", + "stromanthe-mosaic-virus", + "stromanthe-wilt-fusarium-or-verticillium", + "stromanthe-root-knot-nematode", + "stromanthe-canker-stembranch", + "stromanthe-bacterial-soft-rot", + "stromanthe-downy-mildew-generic", + "stromanthe-viral-leaf-curl", + "stromanthe-wood-rot-decay", + "bok-choy-shanghai-clubroot", + "bok-choy-shanghai-black-rot-brassicas", + "bok-choy-shanghai-root-rot-pythiumphytophthora", + "bok-choy-shanghai-damping-off", + "bok-choy-shanghai-gray-mold-botrytis-blight", + "bok-choy-shanghai-mosaic-virus", + "bok-choy-shanghai-wilt-fusarium-or-verticillium", + "bok-choy-shanghai-root-knot-nematode", + "bok-choy-shanghai-canker-stembranch", + "bok-choy-shanghai-bacterial-soft-rot", + "bok-choy-shanghai-downy-mildew-generic", + "bok-choy-shanghai-viral-leaf-curl", + "bok-choy-shanghai-wood-rot-decay", + "tatsoi-clubroot", + "tatsoi-black-rot-brassicas", + "tatsoi-root-rot-pythiumphytophthora", + "tatsoi-damping-off", + "tatsoi-gray-mold-botrytis-blight", + "tatsoi-mosaic-virus", + "tatsoi-wilt-fusarium-or-verticillium", + "tatsoi-root-knot-nematode", + "tatsoi-canker-stembranch", + "tatsoi-bacterial-soft-rot", + "tatsoi-downy-mildew-generic", + "tatsoi-viral-leaf-curl", + "tatsoi-wood-rot-decay", + "mizuna-clubroot", + "mizuna-black-rot-brassicas", + "mizuna-root-rot-pythiumphytophthora", + "mizuna-damping-off", + "mizuna-gray-mold-botrytis-blight", + "mizuna-mosaic-virus", + "mizuna-wilt-fusarium-or-verticillium", + "mizuna-root-knot-nematode", + "mizuna-canker-stembranch", + "mizuna-bacterial-soft-rot", + "mizuna-downy-mildew-generic", + "mizuna-viral-leaf-curl", + "mizuna-wood-rot-decay", + "kohlrabi-clubroot", + "kohlrabi-black-rot-brassicas", + "kohlrabi-root-rot-pythiumphytophthora", + "kohlrabi-damping-off", + "kohlrabi-gray-mold-botrytis-blight", + "kohlrabi-mosaic-virus", + "kohlrabi-wilt-fusarium-or-verticillium", + "kohlrabi-root-knot-nematode", + "kohlrabi-canker-stembranch", + "kohlrabi-bacterial-soft-rot", + "kohlrabi-downy-mildew-generic", + "kohlrabi-viral-leaf-curl", + "kohlrabi-wood-rot-decay", + "rapini-clubroot", + "rapini-black-rot-brassicas", + "rapini-root-rot-pythiumphytophthora", + "rapini-damping-off", + "rapini-gray-mold-botrytis-blight", + "rapini-mosaic-virus", + "rapini-wilt-fusarium-or-verticillium", + "rapini-root-knot-nematode", + "rapini-canker-stembranch", + "rapini-bacterial-soft-rot", + "rapini-downy-mildew-generic", + "rapini-viral-leaf-curl", + "rapini-wood-rot-decay", + "jicama-white-mold-sclerotinia-rot", + "jicama-charcoal-rot", + "jicama-root-rot-pythiumphytophthora", + "jicama-damping-off", + "jicama-gray-mold-botrytis-blight", + "jicama-mosaic-virus", + "jicama-wilt-fusarium-or-verticillium", + "jicama-root-knot-nematode", + "jicama-canker-stembranch", + "jicama-bacterial-soft-rot", + "jicama-downy-mildew-generic", + "jicama-viral-leaf-curl", + "jicama-wood-rot-decay", + "adzuki-bean-white-mold-sclerotinia-rot", + "adzuki-bean-charcoal-rot", + "adzuki-bean-root-rot-pythiumphytophthora", + "adzuki-bean-damping-off", + "adzuki-bean-gray-mold-botrytis-blight", + "adzuki-bean-mosaic-virus", + "adzuki-bean-wilt-fusarium-or-verticillium", + "adzuki-bean-root-knot-nematode", + "adzuki-bean-canker-stembranch", + "adzuki-bean-bacterial-soft-rot", + "adzuki-bean-downy-mildew-generic", + "adzuki-bean-viral-leaf-curl", + "adzuki-bean-wood-rot-decay", + "mung-bean-white-mold-sclerotinia-rot", + "mung-bean-charcoal-rot", + "mung-bean-root-rot-pythiumphytophthora", + "mung-bean-damping-off", + "mung-bean-gray-mold-botrytis-blight", + "mung-bean-mosaic-virus", + "mung-bean-wilt-fusarium-or-verticillium", + "mung-bean-root-knot-nematode", + "mung-bean-canker-stembranch", + "mung-bean-bacterial-soft-rot", + "mung-bean-downy-mildew-generic", + "mung-bean-viral-leaf-curl", + "mung-bean-wood-rot-decay", + "garbanzo-white-mold-sclerotinia-rot", + "garbanzo-charcoal-rot", + "garbanzo-root-rot-pythiumphytophthora", + "garbanzo-damping-off", + "garbanzo-gray-mold-botrytis-blight", + "garbanzo-mosaic-virus", + "garbanzo-wilt-fusarium-or-verticillium", + "garbanzo-root-knot-nematode", + "garbanzo-canker-stembranch", + "garbanzo-bacterial-soft-rot", + "garbanzo-downy-mildew-generic", + "garbanzo-viral-leaf-curl", + "garbanzo-wood-rot-decay", + "wiki-acrophialophora-wilt", + "wiki-aerial-blight", + "wiki-aerial-stem-rot", + "wiki-almond-brown-line-and-decline", + "wiki-almond-hull-rot", + "wiki-alternaria-blight", + "wiki-alternaria-canker", + "wiki-alternaria-late-blight", + "wiki-alternaria-stem-blight", + "wiki-annosum-root-rot", + "wiki-anthracnose-leaf-blight", + "wiki-anthracnose-stalk-rot", + "wiki-anthracnose-top-dieback", + "wiki-aphid-blight", + "wiki-apple-bitter-rot", + "wiki-apple-black-rot-canker", + "wiki-apple-calyx-end-rot", + "wiki-apple-collar-rot", + "wiki-apple-crown-rot", + "wiki-apple-internal-bark-necrosis", + "wiki-apple-monilia-leaf-blight", + "wiki-apple-perennial-canker", + "wiki-apple-ring-rot", + "wiki-apple-southern-blight", + "wiki-apple-spy-decline", + "wiki-apple-white-rot", + "wiki-armillaria-corn-rot", + "wiki-armillaria-crown-and-root-rot", + "wiki-ascochyta-foot-rot", + "wiki-ascochyta-leaf-blight", + "wiki-ascospora-dieback", + "wiki-asparagus-decline-virus", + "wiki-aspergillus-ear-rot", + "wiki-aspergillus-fruit-rot", + "wiki-autogenic-necrosis", + "wiki-bacterial-black-rot", + "wiki-bacterial-blight-of-carrot", + "wiki-bacterial-blight-of-pea", + "wiki-bacterial-hyperplastic-canker", + "wiki-bacterial-necrosis", + "wiki-bacterial-panicle-blight", + "wiki-bacterial-pod-rot", + "wiki-bacterial-slow-wilt", + "wiki-bacterial-soft-rot-of-carrot", + "wiki-bacterial-soft-rot-of-onion", + "wiki-bacterial-stem-gall", + "wiki-bacterial-stem-rot", + "wiki-bacterial-stripe-blight", + "wiki-bacterial-vascular-necrosis-and-rot", + "wiki-band-canker", + "wiki-bark-canker", + "wiki-barker-canker", + "wiki-barley-common-root-rot", + "wiki-basal-stem-and-crown-rot", + "wiki-basal-stem-blight", + "wiki-bitter-rot-of-apple", + "wiki-black-root-and-stem-rot", + "wiki-black-root-rot-of-carrot", + "wiki-black-rot-of-apple", + "wiki-black-rot-of-cabbage", + "wiki-black-stem-rot", + "wiki-black-streak-root-rot", + "wiki-bleaching-necrosis", + "wiki-bleeding-canker", + "wiki-blossom-and-shoot-blight", + "wiki-boll-blight", + "wiki-boll-rot", + "wiki-boll-rot-complex", + "wiki-botryodiplodia-blight", + "wiki-botryodiplodia-canker", + "wiki-botryosphaeria-cane-canker", + "wiki-botryosphaeria-canker", + "wiki-botryosphaeria-dieback", + "wiki-botryosphaeria-stem-canker", + "wiki-botryosphaeria-stem-rot", + "wiki-botrytis-bunch-rot", + "wiki-botrytis-fruit-rot", + "wiki-botrytis-hard-rot", + "wiki-botrytis-head-rot", + "wiki-botrytis-leaf-and-petal-blight", + "wiki-botrytis-petal-blight", + "wiki-boysenberry-decline", + "wiki-bract-and-flower-blight", + "wiki-bract-necrosis", + "wiki-brand-canker", + "wiki-brown-canker", + "wiki-brown-girdling-root-rot", + "wiki-brown-rot-blossom-and-twig-blight", + "wiki-brown-rot-blossom-blight", + "wiki-brown-stem-blight", + "wiki-buckeye-rot", + "wiki-bud-and-twig-blight", + "wiki-bulb-rot", + "wiki-bunch-rot", + "wiki-calyx-end-rot-of-apple", + "wiki-calyx-rot", + "wiki-camarosporium-shoot-and-panicle-blight", + "wiki-cane-blight", + "wiki-cane-gall", + "wiki-canker-rots", + "wiki-cephalosporium-root-rot", + "wiki-ceratocystis-fruit-rot", + "wiki-ceratosystis-canker", + "wiki-chalara-root-rot", + "wiki-cherelle-wilt", + "wiki-cherry-wilt", + "wiki-chestnut-blight", + "wiki-choanephora-rot", + "wiki-christmas-cactus-necrosis", + "wiki-citrus-gummosis", + "wiki-cladosporium-blight", + "wiki-cladosporium-ear-rot", + "wiki-cladosporium-leaf-blight", + "wiki-cladosporium-rot", + "wiki-cladosporium-stem-canker", + "wiki-cocoa-necrosis", + "wiki-coconut-lethal-yellowing", + "wiki-coffee-wilt", + "wiki-coffee-wilt-disease", + "wiki-collar-rot-of-apple", + "wiki-colletotrichum-blight", + "wiki-colletotrichum-stem-canker", + "wiki-colluvial-blight", + "wiki-common-bacterial-blight", + "wiki-common-bean-bacterial-blight", + "wiki-common-root-rot", + "wiki-coniothyrium-canker", + "wiki-coriander-stem-gall", + "wiki-corm-dry-rot", + "wiki-corn-bacterial-stalk-rot", + "wiki-corn-ear-rot", + "wiki-corn-leaf-blight", + "wiki-corn-lethal-necrosis", + "wiki-corn-northern-leaf-blight", + "wiki-corn-southern-leaf-blight", + "wiki-corn-stewart-wilt", + "wiki-corticium-root-rot", + "wiki-corynespora-blight", + "wiki-coryneum-blight", + "wiki-cotton-anthracnose-boll-rot", + "wiki-cotton-boll-rot", + "wiki-cotton-root-rot", + "wiki-cottony-stem-rot", + "wiki-cowpea-severe-mosaic", + "wiki-cranberry-fruit-rot", + "wiki-cranberry-twig-blight", + "wiki-crown-and-cane-gall", + "wiki-crown-and-stem-rot", + "wiki-crown-gall-of-apple", + "wiki-crown-root-rot", + "wiki-crown-rot-of-apple", + "wiki-cucumber-bacterial-wilt", + "wiki-cucumber-necrosis", + "wiki-curvularia-leaf-blight", + "wiki-cushion-gall", + "wiki-cutting-rot", + "wiki-cylindrocarpon-canker", + "wiki-cylindrocarpon-root-and-crown-rot", + "wiki-cylindrocladiella-root-rot", + "wiki-cylindrocladium-blight", + "wiki-cylindrocladium-root-rot", + "wiki-cylindrosporium-blight", + "wiki-cytosporina-canker", + "wiki-daffodil-bulb-rot", + "wiki-dahlia-wilt", + "wiki-datura-necrosis", + "wiki-delphinium-crown-rot", + "wiki-dieback-and-canker", + "wiki-diplodia-boll-rot", + "wiki-diplodia-collar-rot", + "wiki-diplodia-ear-rot", + "wiki-diplodia-fruit-rot", + "wiki-diplodia-root-and-stem-rot", + "wiki-diplodia-stem-canker", + "wiki-diplodia-stem-end-rot", + "wiki-dogwood-canker", + "wiki-dothiorella-blight", + "wiki-dothiorella-canker", + "wiki-dwarf-cavendish-tip-rot", + "wiki-ear-blight", + "wiki-ear-rot", + "wiki-eastern-filbert-blight", + "wiki-eggplant-verticillium-wilt", + "wiki-elm-phloem-necrosis", + "wiki-endothia-canker", + "wiki-english-walnut-blight", + "wiki-eucalyptus-canker", + "wiki-eucalyptus-dieback", + "wiki-eucalyptus-shoot-blight", + "wiki-european-apple-canker", + "wiki-european-canker-of-apple", + "wiki-european-decline", + "wiki-european-filbert-blight", + "wiki-european-larch-canker", + "wiki-eutypa-canker-of-grape", + "wiki-eutypella-canker", + "wiki-exserohilum-leaf-blight", + "wiki-finger-tip-rot", + "wiki-flower-blight", + "wiki-foamy-canker", + "wiki-foliage-blight", + "wiki-fruit-rots", + "wiki-fruitlet-core-rot", + "wiki-fungal-root-rot", + "wiki-fusarium-bud-rot", + "wiki-fusarium-canker", + "wiki-fusarium-crown-rot", + "wiki-fusarium-cutting-rot", + "wiki-fusarium-ear-rot", + "wiki-fusarium-foot-rot", + "wiki-fusarium-head-blight", + "wiki-fusarium-kernel-rot", + "wiki-fusarium-leaf-blight", + "wiki-fusarium-root-and-crown-rot", + "wiki-fusarium-rot", + "wiki-fusarium-seedling-rot", + "wiki-fusarium-stem-canker", + "wiki-fusarium-stem-rot", + "wiki-fusarium-yellows-and-root-rot", + "wiki-fuscous-blight", + "wiki-fusicoccum-canker", + "wiki-ganoderma-root-rot", + "wiki-gibberella-ear-rot", + "wiki-gibberella-stalk-rot", + "wiki-glomerella-canker", + "wiki-glomerella-stem-rot", + "wiki-gnomonia-cane-canker", + "wiki-gray-blight", + "wiki-gum-canker", + "wiki-head-blight", + "wiki-head-rot", + "wiki-hemp-canker", + "wiki-horse-hair-blight", + "wiki-hull-rot", + "wiki-hymenochaete-canker", + "wiki-hypoxylon-canker", + "wiki-inflorescence-rot", + "wiki-internal-bark-necrosis-of-apple", + "wiki-javanese-vascular-wilt", + "wiki-kernel-blight", + "wiki-kernel-rot", + "wiki-lasiodiplodia-leaf-and-stem-rot", + "wiki-lasiodiplodia-pod-rot", + "wiki-lasiodiplodia-vine-decline", + "wiki-leader-dieback", + "wiki-leaf-and-flower-gall", + "wiki-leaf-dieback", + "wiki-leaf-necrosis", + "wiki-leafy-gall", + "wiki-lenticel-rot", + "wiki-leptosphaeria-blight", + "wiki-lethal-necrosis", + "wiki-leucostoma-canker", + "wiki-macrophoma-pod-rot", + "wiki-macrophoma-stem-canker", + "wiki-macrophomina-stem-canker", + "wiki-main-stalk-rot", + "wiki-marasmiellus-rot", + "wiki-marginal-necrosis", + "wiki-massaria-canker", + "wiki-mealybug-wilt", + "wiki-midge-blight", + "wiki-monilia-leaf-blight-of-apple", + "wiki-monosporascus-root-rot", + "wiki-mycosphaerella-blight", + "wiki-neck-rot", + "wiki-nematode-root-rot", + "wiki-neocosmospora-root-rot", + "wiki-nigrospora-ear-rot", + "wiki-noble-rot", + "wiki-north-american-raspberry-decline", + "wiki-nursery-blight", + "wiki-omphalia-root-rot", + "wiki-ophiobolus-stem-canker", + "wiki-ovulinia-petal-blight", + "wiki-ozonium-collar-rot", + "wiki-ozonium-wilt", + "wiki-panicle-and-shoot-blight", + "wiki-panicle-blight", + "wiki-pea-stem-rot", + "wiki-peanut-bud-necrosis", + "wiki-pear-leaf-blight", + "wiki-peduncle-rot", + "wiki-penicillium-rot", + "wiki-perennial-canker-of-apple", + "wiki-petal-blight", + "wiki-phialophora-wilt", + "wiki-phoma-boll-rot", + "wiki-phoma-canker", + "wiki-phoma-root-rot", + "wiki-phoma-stalk-rot", + "wiki-phoma-stem-canker", + "wiki-phoma-wilt", + "wiki-phomopsis-brown-stem-rot", + "wiki-phomopsis-cane-canker", + "wiki-phomopsis-canker", + "wiki-phomopsis-dieback", + "wiki-phomopsis-leaf-blight", + "wiki-phomopsis-rot", + "wiki-phomopsis-seed-rot", + "wiki-phomopsis-shoot-blight", + "wiki-phomopsis-stem-canker", + "wiki-physoderma-stalk-rot", + "wiki-phytophthora-canker", + "wiki-phytophthora-collar-rot", + "wiki-phytophthora-dieback", + "wiki-phytophthora-gummosis", + "wiki-phytophthora-root-and-crown-rot", + "wiki-phytophthora-shuck-and-kernel-rot", + "wiki-phytophthora-stem-canker", + "wiki-phytophthora-trunk-and-bark-canker", + "wiki-phytophthora-wet-rot", + "wiki-phytophthora-wilt", + "wiki-pink-rot-of-inflorescence", + "wiki-pod-rot", + "wiki-post-harvest-soft-rot", + "wiki-post-harvest-root-rot", + "wiki-pseudomonas-blight", + "wiki-pseudostem-heart-rot", + "wiki-pythium-blight", + "wiki-pythium-brown-rot", + "wiki-pythium-fruit-rot", + "wiki-pythium-pod-rot", + "wiki-pythium-root-and-seedling-rot", + "wiki-red-kernel-rot", + "wiki-red-rot-of-leaf-sheath", + "wiki-rhizoctonia-blight", + "wiki-rhizoctonia-boll-rot", + "wiki-rhizoctonia-ear-rot", + "wiki-rhizoctonia-foliar-blight", + "wiki-rhizoctonia-head-rot", + "wiki-rhizoctonia-limb-rot", + "wiki-rhizoctonia-pod-rot", + "wiki-rhizoctonia-root-and-crown-rot", + "wiki-rhizoctonia-sheath-blight", + "wiki-rhizoctonia-stem-canker", + "wiki-rhizoctonia-stem-rot", + "wiki-rhizome-rot", + "wiki-rhizopus-ear-rot", + "wiki-rhizopus-root-rot", + "wiki-rice-gall-dwarf", + "wiki-rice-necrosis-mosaic", + "wiki-rice-wilted-stunt", + "wiki-rigidopurus-root-rot", + "wiki-rind-necrosis", + "wiki-ring-rot-of-apple", + "wiki-root-and-rhizome-rot", + "wiki-root-and-stem-rot", + "wiki-root-gall-smut", + "wiki-root-rot-of-apple", + "wiki-root-tip-rot", + "wiki-rose-canker", + "wiki-rosy-canker", + "wiki-sapwood-rot", + "wiki-schizoxylon-canker", + "wiki-sclerotial-rot", + "wiki-sclerotinia-boll-rot", + "wiki-sclerotinia-crown-and-root-rot", + "wiki-sclerotinia-crown-rot", + "wiki-sclerotinia-flower-rot", + "wiki-sclerotinia-fruit-rot", + "wiki-sclerotinia-head-rot", + "wiki-sclerotinia-shoot-blight", + "wiki-sclerotinia-stalk-rot", + "wiki-sclerotinia-wilt", + "wiki-sclerotium-ear-rot", + "wiki-sclerotium-root-rot", + "wiki-seed-gall-nematode", + "wiki-seed-rot", + "wiki-seedling-or-seed-rot", + "wiki-seedling-rot", + "wiki-shoot-blight", + "wiki-shoot-dieback", + "wiki-shuck-decline", + "wiki-southern-blight-of-apple", + "wiki-southern-corn-leaf-blight", + "wiki-southern-stem-rot", + "wiki-southern-wilt", + "wiki-spur-blight", + "wiki-spy-decline", + "wiki-spy-decline-of-apple", + "wiki-stalk-rot", + "wiki-stamen-blight", + "wiki-stem-and-stolon-canker", + "wiki-stem-and-tuber-rot", + "wiki-stem-blight", + "wiki-stem-end-blight", + "wiki-stem-mold-and-rot", + "wiki-stemphylium-blight", + "wiki-stewart-s-wilt", + "wiki-storage-rot-of-apple", + "wiki-strawberry-root-rot", + "wiki-sweetgum-blight", + "wiki-sydowiella-cane-canker", + "wiki-synchytrium-brown-gall", + "wiki-synchytrium-orange-gall", + "wiki-syringae-seedling-blight", + "wiki-terminal-bud-rot", + "wiki-terminal-shoot-necrosis", + "wiki-texas-root-rot", + "wiki-thielaviopsis-root-rot", + "wiki-thyrostroma-canker", + "wiki-top-rot", + "wiki-trachysphaera-finger-rot", + "wiki-trachysphaera-pot-rot", + "wiki-trichoderma-ear-rot", + "wiki-trichoderma-foot-rot", + "wiki-trichoderma-stalk-rot", + "wiki-tropical-rot", + "wiki-trunk-canker", + "wiki-tuber-rot", + "wiki-tubercularia-canker", + "wiki-twig-dieback", + "wiki-valsa-canker-of-apple", + "wiki-vascular-streak-dieback", + "wiki-vascular-wilt", + "wiki-vein-necrosis", + "wiki-verticillium-root-and-stem-rot", + "wiki-verticillium-tip-rot", + "wiki-victoria-blight", + "wiki-walnut-blight", + "wiki-wet-leaf-rot", + "wiki-wet-root-rot", + "wiki-white-blight", + "wiki-white-ear-rot", + "wiki-white-mold-stem-rot", + "wiki-white-rot-of-apple", + "wiki-white-stem-blight", + "wiki-winter-crown-rot", + "wiki-winter-rot", + "wiki-xanthomonas-wilt", + "wiki-y-center-rot", + "wiki-yellow-wilt", + "wiki-zonate-canker", + "tomato-alternaria-stem-canker", + "tomato-anthracnose", + "tomato-black-mold-rot", + "tomato-black-root-rot", + "tomato-black-shoulder", + "tomato-buckeye-rot-of-tomato", + "tomato-cercospora-leaf-mold", + "tomato-charcoal-rot", + "tomato-corky-root-rot", + "tomato-didymella-stem-rot", + "tomato-early-blight", + "tomato-fusarium-crown-and-root-rot", + "tomato-fusarium-wilt", + "tomato-gray-leaf-spot", + "tomato-gray-mold", + "tomato-late-blight", + "tomato-leaf-mold", + "tomato-phoma-rot", + "tomato-pythium-damping-off-and-fruit-rot", + "tomato-rhizoctonia-damping-off-and-fruit-rot", + "tomato-rhizopus-rot", + "tomato-septoria-leaf-spot", + "tomato-sour-rot", + "tomato-southern-blight", + "tomato-target-spot", + "tomato-verticillium-wilt", + "tomato-white-mold", + "potato-black-dot", + "potato-brown-spot-and-black-pit", + "potato-cercospora-leaf-blotch", + "potato-charcoal-rot", + "potato-choanephora-blight", + "potato-deforming-rust", + "potato-early-blight", + "potato-fusarium-dry-rot", + "potato-fusarium-wilt", + "potato-gray-mold", + "potato-phoma-leaf-spot", + "potato-powdery-mildew", + "potato-rhizoctonia-canker-and-black-scurf", + "potato-rosellinia-black-rot", + "potato-septoria-leaf-spot", + "potato-silver-scurf", + "potato-skin-spot", + "potato-stem-rot-southern-blight", + "potato-thecaphora-smut", + "potato-ulocladium-blight", + "potato-verticillium-wilt", + "potato-white-mold", + "potato-", + "apple-alternaria-blotch", + "apple-alternaria-rot", + "apple-american-brown-rot", + "apple-anthracnose-canker-and-bulls-eye-rot", + "apple-apple-scab", + "apple-apple-ring-rot-and-canker", + "apple-armillaria-root-rot-shoestring-root-rot", + "apple-bitter-rot", + "apple-black-pox", + "apple-black-root-rot", + "apple-black-rot-frogeye-leafspot-and-canker", + "apple-blister-canker-nailhead-canker", + "apple-blue-mold", + "apple-brooks-fruit-spot", + "apple-brown-rot-blossom-blight-and-spur-infection", + "apple-calyx-end-rot", + "apple-clitocybe-root-rot", + "apple-diaporthe-canker", + "apple-diplodia-canker", + "apple-european-brown-rot", + "apple-fisheye-rot", + "apple-flyspeck", + "apple-fruit-blotch-leaf-spot-and-twig-canker", + "apple-glomerella-leaf-spot", + "apple-gray-mold-rot-dry-eye-rot-blossom-end-rot", + "apple-leptosphaeria-canker-and-fruit-rot", + "apple-leucostoma-canker-and-dieback", + "apple-marssonina-blotch", + "apple-moldy-core-and-core-rot", + "apple-monilia-leaf-blight", + "apple-monochaetia-twig-canker", + "apple-mucor-rot", + "apple-nectria-canker", + "apple-nectria-twig-blight-coral-spot", + "apple-peniophora-root-canker", + "apple-perennial-canker", + "apple-phomopsis-canker-fruit-decay-and-rough-bark", + "apple-phymatotrichum-root-rot-cotton-root-rot", + "apple-phytophthora-crown-collar-and-root-rot-sprinkler-rot", + "apple-phytophthora-fruit-rot", + "apple-pink-mold-rot", + "apple-powdery-mildew", + "apple-rosellinia-root-rot-dematophora-root-rot", + "apple-rubber-rot", + "apple-american-hawthorne-rust", + "apple-cedar-apple-rust", + "apple-japanese-apple-rust", + "apple-pacific-coast-pear-rust", + "apple-quince-rust", + "apple-side-rot", + "apple-silver-leaf", + "apple-sooty-blotch-complex", + "apple-southern-blight", + "apple-thread-blight-hypochnus-leaf-blight", + "apple-valsa-canker", + "apple-violet-root-rot", + "apple-white-root-rot", + "apple-white-rot", + "apple-x-spot-nigrospora-spot", + "apple-zonate-leaf-spot", + "apricot-alternaria-spot-and-fruit-rot", + "apricot-armillaria-crown-and-root-rot-shoestring-crown-and-root-rot", + "apricot-brown-rot-blossom-and-twig-blight-and-fruit-rot", + "apricot-ceratocystis-canker", + "apricot-cytospora-canker", + "apricot-dematophora-root-rot", + "apricot-eutypa-dieback", + "apricot-green-fruit-rot", + "apricot-leaf-spot", + "apricot-phytophthora-crown-and-root-rot", + "apricot-phytophthora-pruning-wound-canker", + "apricot-powdery-mildew", + "apricot-replant-problems", + "apricot-rhizopus-fruit-rot", + "apricot-ripe-fruit-rot", + "apricot-scab", + "apricot-shot-hole", + "apricot-silver-leaf", + "apricot-verticillium-wilt", + "apricot-wood-rots-pathogenicity-has-not-been-proven-for-these-fungi", + "avocado-anthracnose", + "avocado-armillaria-root-rot-shoestring-root-rot", + "avocado-black-mildew", + "avocado-branch-canker", + "avocado-butt-rot", + "avocado-cercospora-spot-blotch", + "avocado-clitocybe-root-rot", + "avocado-collar-rot", + "avocado-dematophora-root-rot", + "avocado-dieback", + "avocado-fruit-rot-includes-stem-end-rot-fruit-spots", + "avocado-leaf-spots", + "avocado-phomopsis-spot", + "avocado-physalospora-canker", + "avocado-phytophthora-crown-rot", + "avocado-phytophthora-trunk-canker", + "avocado-phytophthora-root-rot", + "avocado-pink-rot", + "avocado-powdery-mildew", + "avocado-rhizoctonia-seed-and-root-rot", + "avocado-root-and-bark-rot", + "avocado-root-rot", + "avocado-rosellinia-root-rot", + "avocado-rusty-blight", + "avocado-scab-fruit-leaf", + "avocado-seedling-blight", + "avocado-smudgy-spot", + "avocado-sooty-blotch", + "avocado-tar-spot", + "avocado-verticillium-wilt", + "avocado-wood-rots", + "carrot-alternaria-leaf-blight", + "carrot-black-root-rot", + "carrot-black-rot-black-carrot-root-dieback", + "carrot-blue-mold-rot-blue-green-mold", + "carrot-brown-rot-phoma-disease", + "carrot-buckshot-rot", + "carrot-cavity-spot", + "carrot-cercospora-leaf-spot", + "carrot-cottony-rot", + "carrot-crater-rot", + "carrot-crown-rot", + "carrot-dieback-of-carrots", + "carrot-forking-brown-root", + "carrot-fusarium-dry-rot", + "carrot-gray-mold-rot", + "carrot-hard-rot", + "carrot-lateral-root-dieback", + "carrot-leaf-rot", + "carrot-leaf-spot", + "carrot-licorice-rot", + "carrot-phytophthora-root-rot", + "carrot-pink-mold-rot", + "carrot-powdery-mildew", + "carrot-pythium-brown-rot-and-forking", + "carrot-pythium-root-dieback", + "carrot-rhizoctonia-canker", + "carrot-rhizoctonia-seedling-disease", + "carrot-rhizopus-wooly-soft-rot", + "carrot-root-canker", + "carrot-root-dieback", + "carrot-root-rot", + "carrot-phymatotrichum-root-rot-cotton-root-rot", + "carrot-rubbery-brown-rot", + "carrot-rubbery-slate-rot", + "carrot-rusty-root", + "carrot-sclerotinia-rot", + "carrot-seed-mold", + "carrot-sooty-rot", + "carrot-sour-rot", + "carrot-southern-blight", + "carrot-stem-spot", + "carrot-tip-rot", + "carrot-umbel-blight", + "carrot-violet-root-rot", + "carrot-watery-soft-rot", + "citrus-alternaria-brown-spot", + "citrus-alternaria-leaf-spot-of-rough-lemon", + "citrus-alternaria-stem-end-rot", + "citrus-anthracnose-wither-tip", + "citrus-areolate-leaf-spot", + "citrus-black-root-rot", + "citrus-black-rot", + "citrus-black-spot", + "citrus-blue-mold", + "citrus-botrytis-blossom-and-twig-blight-gummosis", + "citrus-branch-knot", + "citrus-brown-rot-fruit", + "citrus-charcoal-root-rot", + "citrus-damping-off", + "citrus-diplodia-gummosis-and-stem-end-rot", + "citrus-dothiorella-gummosis-and-rot", + "citrus-dry-root-rot-complex", + "citrus-dry-rot-fruit", + "citrus-fly-speck", + "citrus-fusarium-rot-fruit", + "citrus-fusarium-wilt", + "citrus-gray-mold-fruit", + "citrus-greasy-spot-and-greasy-spot-rind-blotch", + "citrus-green-mold" ], - "totalFound": 650 + "totalFound": 1800 } \ No newline at end of file diff --git a/apps/web/scripts/fine-tune-model.py b/apps/web/scripts/fine-tune-model.py new file mode 100644 index 0000000..24556ee --- /dev/null +++ b/apps/web/scripts/fine-tune-model.py @@ -0,0 +1,537 @@ +#!/usr/bin/env python3 +""" +fine-tune-model.py + +Fine-tunes the PlantVillage MobileNetV2 model on a custom 95-class dataset +(93 diseases + healthy + unknown). + +Pipeline: + 1. Load `best_mnv2_pv_original.keras` (MobileNetV2 backbone + 38-class head) + 2. Replace the 38-class head with 95 classes (order matches diseases.json + healthy + unknown) + 3. Freeze backbone, train only the new classification head + 4. Unfreeze the last ~20 layers, fine-tune at lower learning rate + 5. Export to TF.js GraphModel format + 6. Export to .keras for future retraining + +Usage: .tfjs-venv/bin/python scripts/fine-tune-model.py +""" + +import json +import os +import sys +import shutil +from pathlib import Path + +os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2" # Suppress TF info/warnings + +import numpy as np +import tensorflow as tf +import keras +from keras import layers, optimizers, regularizers + +# ─── Constants ─────────────────────────────────────────────────────────────── + +PROJECT_ROOT = Path(__file__).resolve().parent.parent +MODEL_PATH = ( + PROJECT_ROOT + / "public" + / "models" + / "plant-disease-classifier" + / "best_mnv2_pv_original.keras" +) +DISEASES_JSON = PROJECT_ROOT / "src" / "data" / "diseases.json" +DATASET_DIR = PROJECT_ROOT / "data" / "dataset" +OUTPUT_DIR = PROJECT_ROOT / "public" / "models" / "plant-disease-classifier" +TFJS_OUTPUT = OUTPUT_DIR / "tfjs_finetuned" + +IMG_SIZE = 160 # Model input size +BATCH_SIZE = 32 +EPOCHS_HEAD = 15 # Train just the new head +EPOCHS_FINETUNE = 10 # Unfreeze and fine-tune +LEARNING_RATE_HEAD = 1e-3 +LEARNING_RATE_FINETUNE = 1e-5 +VALIDATION_SPLIT = 0.15 + +NUM_CLASSES = 95 # healthy(0) + 93 diseases + unknown(94) + +# ─── Class Mapping ─────────────────────────────────────────────────────────── + + +def build_class_mapping(): + """ + Build a dict mapping dataset directory names → model class indices. + Matches the ordering in labels.ts / diseases.json. + + Index 0 = "healthy" + Index 1-93 = disease IDs (in diseases.json order) + Index 94 = "unknown" (no images — skip during training) + """ + with open(DISEASES_JSON) as f: + diseases = json.load(f) + + mapping = {"healthy": 0} + for i, disease in enumerate(diseases): + mapping[disease["id"]] = i + 1 # Index 1-93 + mapping["unknown"] = 94 # Not trained, but reserved + + # Reverse mapping for predictions + index_to_class = {v: k for k, v in mapping.items()} + + return mapping, index_to_class + + +def verify_dataset(mapping): + """Find which classes have images and how many.""" + available = {} + total = 0 + + for class_id, class_idx in mapping.items(): + class_dir = DATASET_DIR / class_id + if not class_dir.exists(): + continue + + image_paths = sorted(class_dir.glob("*")) + image_paths = [ + p + for p in image_paths + if p.suffix.lower() in (".jpg", ".jpeg", ".png", ".webp") + ] + + if image_paths: + available[class_id] = {"index": class_idx, "count": len(image_paths)} + total += len(image_paths) + + return available, total + + +def print_dataset_summary(available, total): + """Print a summary of what's available.""" + print(f"\n{'─' * 60}") + print("DATASET SUMMARY") + print(f"{'─' * 60}") + print(f" Total images: {total}") + print(f" Classes found: {len(available)} / {len(build_class_mapping()[0])}") + print( + f" Missing classes with no images: {len(build_class_mapping()[0]) - len(available)}" + ) + + # Count images per class + counts = [(v["index"], k, v["count"]) for k, v in available.items()] + counts.sort(key=lambda x: x[1]) + + print("\n Images per class:") + for idx, class_id, count in counts: + label = f" {idx:3d}. {class_id:<35s} {count:>4d} images" + if class_id == "healthy": + label += " ← 2× target" + print(label) + + # Stats + class_counts = [v["count"] for v in available.values()] + if class_counts: + print( + f"\n Min: {min(class_counts)} Max: {max(class_counts)} Avg: {sum(class_counts) / len(class_counts):.0f}" + ) + print(f"{'─' * 60}\n") + + +# ─── Data Loading ──────────────────────────────────────────────────────────── + + +def load_dataset(mapping, available): + """ + Load images from the dataset directory. + Returns train/validation datasets with augmentation. + """ + # Build file paths and labels + file_paths = [] + labels = [] + + for class_id, info in available.items(): + class_dir = DATASET_DIR / class_id + images = sorted(class_dir.glob("*")) + images = [ + p for p in images if p.suffix.lower() in (".jpg", ".jpeg", ".png", ".webp") + ] + + for img_path in images: + file_paths.append(str(img_path)) + labels.append(info["index"]) + + file_paths = np.array(file_paths) + labels = np.array(labels) + + # Shuffle + indices = np.random.RandomState(42).permutation(len(file_paths)) + file_paths = file_paths[indices] + labels = labels[indices] + + # Split train/validation + split = int(len(file_paths) * (1 - VALIDATION_SPLIT)) + train_paths, val_paths = file_paths[:split], file_paths[split:] + train_labels, val_labels = labels[:split], labels[split:] + + print(f" Train: {len(train_paths)} images") + print(f" Val: {len(val_paths)} images") + + # Parse function + def parse_image(image_path, label): + img = tf.io.read_file(image_path) + img = tf.image.decode_image(img, channels=3, expand_animations=False) + img = tf.image.resize(img, [IMG_SIZE, IMG_SIZE]) + img = tf.cast(img, tf.float32) / 255.0 + # ImageNet normalization (matching training-time preprocessing) + mean = tf.constant([0.485, 0.456, 0.406]) + std = tf.constant([0.229, 0.224, 0.225]) + img = (img - mean) / std + return img, label + + def augment(image, label): + """Data augmentation for training set.""" + # Random horizontal flip + image = tf.image.random_flip_left_right(image) + # Random rotation (±20°) + image = tf.image.random_flip_up_down(image) + # Random brightness + image = tf.image.random_brightness(image, 0.15) + # Random contrast + image = tf.image.random_contrast(image, 0.8, 1.2) + # Random saturation + image = tf.image.random_saturation(image, 0.8, 1.2) + # Random hue + image = tf.image.random_hue(image, 0.05) + # Random crop (after slightly scaling up) + image = tf.image.resize_with_crop_or_pad(image, IMG_SIZE + 12, IMG_SIZE + 12) + image = tf.image.resize(image, [IMG_SIZE, IMG_SIZE]) + # Clip to valid range after augmentations + image = tf.clip_by_value(image, -2.5, 2.5) + return image, label + + # Create tf.data datasets + train_ds = tf.data.Dataset.from_tensor_slices((train_paths, train_labels)) + train_ds = train_ds.map(parse_image, num_parallel_calls=tf.data.AUTOTUNE) + train_ds = train_ds.map(augment, num_parallel_calls=tf.data.AUTOTUNE) + train_ds = train_ds.shuffle(1000).batch(BATCH_SIZE).prefetch(tf.data.AUTOTUNE) + + val_ds = tf.data.Dataset.from_tensor_slices((val_paths, val_labels)) + val_ds = val_ds.map(parse_image, num_parallel_calls=tf.data.AUTOTUNE) + val_ds = val_ds.batch(BATCH_SIZE).prefetch(tf.data.AUTOTUNE) + + return train_ds, val_ds + + +# ─── Model Building ────────────────────────────────────────────────────────── + + +def build_model(): + """ + Load the PlantVillage model and replace the classification head + with a 95-class output. + """ + print(f"\nLoading base model from: {MODEL_PATH}") + if not MODEL_PATH.exists(): + print(f"ERROR: Model not found at {MODEL_PATH}") + sys.exit(1) + + base_model = keras.models.load_model(str(MODEL_PATH)) + print(f" Base model loaded: {type(base_model).__name__}") + print(f" Input shape: {base_model.input_shape}") + print(f" Output shape: {base_model.output_shape}") + + # Extract backbone — everything up to the GlobalAveragePooling2D + # The model structure is: + # input_layer_2 → mobilenetv2_1.00_160 → global_average_pooling2d → dropout → dense(38) + backbone_output = base_model.get_layer("global_average_pooling2d").output + print(" Using backbone output: global_average_pooling2d") + + # Freeze all backbone layers initially + # (we'll unfreeze later for fine-tuning) + for layer in base_model.layers: + if layer.name != "dense": # We'll replace this anyway + layer.trainable = False + + # Build new classification head + x = backbone_output + x = layers.Dropout(0.3, name="dropout_new")(x) + x = layers.Dense( + NUM_CLASSES, + activation="softmax", + name="dense_new", + kernel_regularizer=regularizers.l2(1e-4), + )(x) + + # Create new model + model = keras.Model( + inputs=base_model.input, outputs=x, name="plant-disease-classifier-v2" + ) + + print(f" New model input: {model.input_shape}") + print(f" New model output: {model.output_shape} ({NUM_CLASSES} classes)") + + # Count trainable params + backbone_trainable = sum( + w.shape.num_elements() + for layer in base_model.layers + if layer.name != "dense" + for w in layer.trainable_weights + ) + head_trainable = sum( + w.shape.num_elements() for w in model.get_layer("dense_new").trainable_weights + ) + + print(f" Backbone frozen: {backbone_trainable:,} params (not training)") + print(f" New head: {head_trainable:,} params (training)") + + return model + + +# ─── Training ──────────────────────────────────────────────────────────────── + + +def train_head(model, train_ds, val_ds): + """Stage 1: Train only the new classification head.""" + print(f"\n{'=' * 60}") + print("STAGE 1: Training classification head") + print(f"{'=' * 60}") + print(f" Epochs: {EPOCHS_HEAD}") + print(f" Learning rate: {LEARNING_RATE_HEAD}") + print(f" Batch size: {BATCH_SIZE}") + + # Freeze all backbone layers + for layer in model.layers: + if layer.name != "dense_new": + layer.trainable = False + else: + layer.trainable = True + + # Verify + trainable = sum(w.shape.num_elements() for w in model.trainable_weights) + total = sum(w.shape.num_elements() for w in model.weights) + print(f" Trainable params: {trainable:,} / {total:,} total") + + model.compile( + optimizer=optimizers.Adam(learning_rate=LEARNING_RATE_HEAD), + loss="sparse_categorical_crossentropy", + metrics=["accuracy", "sparse_top_k_categorical_accuracy"], + ) + + history = model.fit( + train_ds, + validation_data=val_ds, + epochs=EPOCHS_HEAD, + verbose=1, + callbacks=[ + keras.callbacks.EarlyStopping( + monitor="val_accuracy", + patience=3, + restore_best_weights=True, + ), + keras.callbacks.ReduceLROnPlateau( + monitor="val_loss", + factor=0.5, + patience=2, + min_lr=1e-6, + ), + ], + ) + + final_val_acc = history.history["val_accuracy"][-1] + print(f"\n Stage 1 complete! Val accuracy: {final_val_acc:.4f}") + return history + + +def train_finetune(model, train_ds, val_ds): + """Stage 2: Unfreeze last ~25 layers and fine-tune.""" + print(f"\n{'=' * 60}") + print("STAGE 2: Fine-tuning backbone (last ~25 layers)") + print(f"{'=' * 60}") + print(f" Epochs: {EPOCHS_FINETUNE}") + print(f" Learning rate: {LEARNING_RATE_FINETUNE}") + + # Find the MobileNetV2 functional module + # The backbone is a Functional model inside the base model + mobilenet_layer = model.get_layer("mobilenetv2_1.00_160") + + # Unfreeze the last ~25 layers of the backbone + total_backbone_layers = len(mobilenet_layer.layers) + unfreeze_from = max(0, total_backbone_layers - 25) + print( + f" Backbone has {total_backbone_layers} layers, unfreezing from layer {unfreeze_from}" + ) + + for i, layer in enumerate(mobilenet_layer.layers): + layer.trainable = i >= unfreeze_from + + # Also unfreeze the new head + model.get_layer("dense_new").trainable = True + model.get_layer("dropout_new").trainable = True + + trainable = sum(w.shape.num_elements() for w in model.trainable_weights) + total = sum(w.shape.num_elements() for w in model.weights) + print(f" Trainable params: {trainable:,} / {total:,} total") + + model.compile( + optimizer=optimizers.Adam(learning_rate=LEARNING_RATE_FINETUNE), + loss="sparse_categorical_crossentropy", + metrics=["accuracy", "sparse_top_k_categorical_accuracy"], + ) + + history = model.fit( + train_ds, + validation_data=val_ds, + epochs=EPOCHS_FINETUNE, + verbose=1, + callbacks=[ + keras.callbacks.EarlyStopping( + monitor="val_accuracy", + patience=3, + restore_best_weights=True, + ), + keras.callbacks.ReduceLROnPlateau( + monitor="val_loss", + factor=0.5, + patience=2, + min_lr=1e-7, + ), + ], + ) + + final_val_acc = history.history["val_accuracy"][-1] + print(f"\n Stage 2 complete! Val accuracy: {final_val_acc:.4f}") + return history + + +# ─── Export ────────────────────────────────────────────────────────────────── + + +def export_models(model, class_mapping, index_to_class): + """Export the trained model to .keras and TF.js formats.""" + print(f"\n{'=' * 60}") + print("EXPORTING") + print(f"{'=' * 60}") + + # 1. Save as .keras (for future retraining) + keras_path = OUTPUT_DIR / "model-finetuned.keras" + model.save(str(keras_path)) + print(f" ✓ Saved .keras: {keras_path}") + + # 2. Save class mapping alongside the model + mapping_path = OUTPUT_DIR / "class_mapping.json" + with open(mapping_path, "w") as f: + json.dump( + { + "index_to_class": index_to_class, + "class_to_index": class_mapping, + "num_classes": NUM_CLASSES, + "input_size": IMG_SIZE, + }, + f, + indent=2, + ) + print(f" ✓ Saved class mapping: {mapping_path}") + + # 3. Export to TF.js format + tfjs_path = str(TFJS_OUTPUT) + if TFJS_OUTPUT.exists(): + shutil.rmtree(tfjs_path) + + try: + import tensorflowjs as tfjs + + tfjs.converters.save_keras_model(model, tfjs_path) + print(f" ✓ Saved TF.js: {tfjs_path}/") + for f in sorted(TFJS_OUTPUT.iterdir()): + size = f.stat().st_size + print(f" {f.name:<30s} {size:>10,} bytes") + except Exception as e: + print(f" ⚠ TF.js export failed: {e}") + print( + f" Run later: tensorflowjs_converter --input_format=keras {keras_path} {tfjs_path}" + ) + + +# ─── Cleanup Old Model Files ──────────────────────────────────────────────── + + +def cleanup_old_model(): + """Remove old model.json and shards from the directory.""" + for f in OUTPUT_DIR.glob("model.json"): + print(f" Removing old: {f.name}") + f.unlink() + for f in OUTPUT_DIR.glob("group1-shard*"): + print(f" Removing old: {f.name}") + f.unlink() + + +# ─── Main ──────────────────────────────────────────────────────────────────── + + +def main(): + print("=" * 60) + print("PLANT DISEASE MODEL FINE-TUNER") + print("=" * 60) + + # 1. Build class mapping + print("\n[1/5] Building class mapping...") + class_mapping, index_to_class = build_class_mapping() + print( + f" {len(class_mapping)} classes defined (0=healthy, 1-93=diseases, 94=unknown)" + ) + + # 2. Verify dataset + print("\n[2/5] Verifying dataset...") + if not DATASET_DIR.exists(): + print(f" ERROR: Dataset not found at {DATASET_DIR}") + print(" Run the scraper first: npx tsx scripts/scrape-training-dataset.ts") + sys.exit(1) + + available, total = verify_dataset(class_mapping) + print_dataset_summary(available, total) + + if total < 100: + print(f" WARNING: Only {total} images. Consider scraping more data.") + print(" Continue anyway? (y/n)") + # Continue regardless — user can decide + + # 3. Load dataset + print("\n[3/5] Loading and augmenting dataset...") + train_ds, val_ds = load_dataset(class_mapping, available) + + # 4. Build and train model + print("\n[4/5] Building model...") + model = build_model() + model.summary() + + # Check if training should run + if total > 0: + train_head(model, train_ds, val_ds) + train_finetune(model, train_ds, val_ds) + + # 5. Export + print("\n[5/5] Exporting models...") + cleanup_old_model() + export_models(model, class_mapping, index_to_class) + else: + print("\n Skipping training — no dataset available.") + sys.exit(1) + + # ── Final Summary ──────────────────────────────────────────────────────── + + print(f"\n{'=' * 60}") + print("DONE! Model fine-tuned and exported.") + print(f"{'=' * 60}") + print("\nFiles created:") + print(f" {OUTPUT_DIR / 'model-finetuned.keras'}") + print(f" {OUTPUT_DIR / 'class_mapping.json'}") + print(f" {TFJS_OUTPUT / 'model.json'}") + print("\nTo update your app:") + print(" 1. Replace model files:") + print(f" cp {TFJS_OUTPUT / 'model.json'} {OUTPUT_DIR / 'model.json'}") + print(f" cp {TFJS_OUTPUT / 'group1-shard*'} {OUTPUT_DIR / '/'}") + print(" 2. Restart the dev server") + print(" 3. Test with: POST /api/identify") + print("\nNote: Update labels.ts if the class order changed.") + + +if __name__ == "__main__": + main() diff --git a/apps/web/scripts/scrape-training-dataset.ts b/apps/web/scripts/scrape-training-dataset.ts new file mode 100644 index 0000000..1cce92f --- /dev/null +++ b/apps/web/scripts/scrape-training-dataset.ts @@ -0,0 +1,660 @@ +#!/usr/bin/env node +/** + * scrape-training-dataset.ts + * + * Collects a training dataset for fine-tuning by scraping DuckDuckGo image search. + * + * Targets: + * - 200 images per disease class (93 diseases) + * - 400 images for the "healthy" class + * - Full resolution images stored in data/dataset/{class_id}/ + * + * DuckDuckGo approach (no API key needed): + * 1. Fetch the main search page to extract a vqd (query) token + * 2. Use the vqd token to paginate through image results + * 3. Download each image to the dataset directory + * + * Usage: cd apps/web && npx tsx scripts/scrape-training-dataset.ts + * + * Progress is tracked in data/dataset/.progress.json — interrupt and resume safely. + */ + +import "dotenv/config"; +import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs"; +import { resolve, extname, join } from "path"; + +// ─── Config ───────────────────────────────────────────────────────────────── + +const DISEASES_JSON = resolve(__dirname, "../src/data/diseases.json"); +const PLANTS_JSON = resolve(__dirname, "../src/data/plants.json"); + +const DATASET_DIR = resolve(__dirname, "../data/dataset"); +const PROGRESS_FILE = resolve(DATASET_DIR, ".progress.json"); + +/** Target images per disease class */ +const TARGET_PER_DISEASE = 200; + +/** Target images for the "healthy" class (2× normal) */ +const TARGET_HEALTHY = 400; + +/** Delay between DuckDuckGo search API calls (ms) */ +const SEARCH_DELAY = 1500; + +/** Delay between image downloads (ms) */ +const DOWNLOAD_DELAY = 300; + +/** Max concurrent downloads */ +const CONCURRENT_DOWNLOADS = 5; + +/** Minimum image size in bytes to accept (reject tiny placeholders) */ +const MIN_IMAGE_SIZE = 10_000; // 10KB + +/** Maximum image size in bytes */ +const MAX_IMAGE_SIZE = 10 * 1024 * 1024; // 10MB + +/** Allowed image content types */ +const ALLOWED_CONTENT_TYPES = ["image/jpeg", "image/jpg", "image/png", "image/webp", "image/gif"]; + +/** Allowed file extensions */ +const ALLOWED_EXTENSIONS = [".jpg", ".jpeg", ".png", ".webp"]; + +/** User agent for requests */ +const UA = + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"; + +// ─── Types ────────────────────────────────────────────────────────────────── + +interface DiseaseSeed { + id: string; + plantId: string; + name: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [key: string]: any; +} + +interface PlantSeed { + id: string; + commonName: string; + scientificName: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [key: string]: any; +} + +interface DuckDuckGoImageResult { + image: string; + title: string; + url: string; + thumbnail: string; + height: number; + width: number; +} + +interface ClassProgress { + count: number; + downloaded: number; + failed: number; + skipped: number; + /** URLs we've already seen (to avoid duplicates) */ + seenUrls: string[]; + /** Whether we've exhausted search results */ + exhausted: boolean; +} + +interface Progress { + lastUpdated: string; + classes: Record; +} + +/** Class ID for healthy plants */ +const HEALTHY_CLASS = "healthy"; + +// ─── DuckDuckGo API ───────────────────────────────────────────────────────── + +/** + * Extract the vqd token from DuckDuckGo's search page. + * Required for paginating image results. + */ +async function getVqdToken(query: string): Promise { + const url = `https://duckduckgo.com/?q=${encodeURIComponent(query)}&t=h_&iax=images&ia=images`; + + const res = await fetch(url, { + headers: { "User-Agent": UA, Accept: "text/html" }, + signal: AbortSignal.timeout(15_000), + }); + + if (!res.ok) { + throw new Error(`Failed to get vqd token: ${res.status}`); + } + + const html = await res.text(); + + // Extract vqd token from the HTML + // Format: vqd='' or vqd="" + const match = html.match(/vqd['"]?\s*[:=]\s*['"]([a-f0-9-]+)['"]/); + if (!match) { + throw new Error(`Could not extract vqd token from DuckDuckGo response for "${query}"`); + } + + return match[1]; +} + +/** + * Fetch a page of DuckDuckGo image results. + */ +async function searchImagesDuckDuckGo( + query: string, + vqd: string, + page: number, +): Promise { + const url = `https://duckduckgo.com/i.js?q=${encodeURIComponent(query)}&vqd=${vqd}&o=json&p=${page}&f=,,,`; + + const res = await fetch(url, { + headers: { + "User-Agent": UA, + Accept: "application/json", + Referer: `https://duckduckgo.com/?q=${encodeURIComponent(query)}&t=h_&iax=images&ia=images`, + }, + signal: AbortSignal.timeout(15_000), + }); + + if (!res.ok) { + if (res.status === 429) { + console.warn(" ⚠ Rate limited (429). Waiting 10s..."); + await sleep(10_000); + return searchImagesDuckDuckGo(query, vqd, page); // Retry + } + if (res.status === 403) { + console.warn(" ⚠ Forbidden (403). Token may have expired."); + return []; // Token expired — no more pages + } + throw new Error(`DuckDuckGo search failed: ${res.status}`); + } + + const data = (await res.json()) as { results: DuckDuckGoImageResult[] }; + return data.results ?? []; +} + +/** + * Search DuckDuckGo images, automatically paginating to collect up to `target` results. + * Returns unique image URLs. + */ +async function collectImages( + query: string, + target: number, + seenUrls: Set, +): Promise<{ urls: string[]; exhausted: boolean }> { + const results: string[] = []; + let page = 1; + let exhausted = false; + let consecutiveEmpty = 0; + + // Get vqd token + let vqd: string; + try { + vqd = await getVqdToken(query); + } catch (err) { + console.warn(` ⚠ Failed to get vqd token: ${err instanceof Error ? err.message : "unknown"}`); + return { urls: [], exhausted: true }; + } + + while (results.length < target) { + await sleep(SEARCH_DELAY); + + let pageResults: DuckDuckGoImageResult[]; + try { + pageResults = await searchImagesDuckDuckGo(query, vqd, page); + } catch (err) { + console.warn(` ⚠ Search error: ${err instanceof Error ? err.message : "unknown"}`); + break; + } + + if (pageResults.length === 0) { + consecutiveEmpty++; + if (consecutiveEmpty >= 3) { + exhausted = true; + break; + } + page++; + continue; + } + + consecutiveEmpty = 0; + let newCount = 0; + + for (const r of pageResults) { + if (results.length >= target) break; + + const imgUrl = r.image || r.url; + + // Skip if we've already seen this URL + if (seenUrls.has(imgUrl)) continue; + + // Validate URL looks like an image + const ext = extname(new URL(imgUrl).pathname).toLowerCase(); + if (!ALLOWED_EXTENSIONS.includes(ext) && !ext) { + // No extension - still try, could be a CDN URL + } + + seenUrls.add(imgUrl); + results.push(imgUrl); + newCount++; + } + + if (newCount === 0 && pageResults.every((r) => seenUrls.has(r.image || r.url))) { + // All results on this page were already seen + page++; + continue; + } + + if (results.length < target) { + page++; + } + } + + return { urls: results.slice(0, target), exhausted }; +} + +// ─── Image Download ───────────────────────────────────────────────────────── + +/** + * Download a single image from a URL to the target path. + * Returns true if successful, false otherwise. + */ +async function downloadImage(url: string, destPath: string): Promise { + try { + const res = await fetch(url, { + headers: { "User-Agent": UA, Accept: "image/webp,image/png,image/jpeg" }, + signal: AbortSignal.timeout(15_000), + }); + + if (!res.ok) return false; + + const contentType = res.headers.get("content-type") || ""; + const contentLength = parseInt(res.headers.get("content-length") || "0", 10); + + // Validate content type + if (!ALLOWED_CONTENT_TYPES.some((t) => contentType.includes(t))) { + return false; + } + + // Validate size + if (contentLength > 0 && contentLength < MIN_IMAGE_SIZE) return false; + if (contentLength > MAX_IMAGE_SIZE) return false; + + const buffer = Buffer.from(await res.arrayBuffer()); + + // Double-check actual buffer size + if (buffer.length < MIN_IMAGE_SIZE) return false; + if (buffer.length > MAX_IMAGE_SIZE) return false; + + // Determine correct extension from content type or URL + let ext = extname(new URL(url).pathname).toLowerCase(); + if (!ALLOWED_EXTENSIONS.includes(ext)) { + // Map from content type + if (contentType.includes("jpeg") || contentType.includes("jpg")) ext = ".jpg"; + else if (contentType.includes("png")) ext = ".png"; + else if (contentType.includes("webp")) ext = ".webp"; + else ext = ".jpg"; // Default + } + + const filePath = destPath.replace(/\.\w+$/, ext); + writeFileSync(filePath, buffer); + return true; + } catch { + return false; + } +} + +/** + * Download multiple images concurrently, respecting a per-download delay. + */ +async function downloadBatch( + urls: string[], + classDir: string, + startIndex: number, +): Promise<{ downloaded: number; failed: number; lastIndex: number }> { + let downloaded = 0; + let failed = 0; + let index = startIndex; + + // Process in chunks to control concurrency + for (let i = 0; i < urls.length; i += CONCURRENT_DOWNLOADS) { + const chunk = urls.slice(i, i + CONCURRENT_DOWNLOADS); + + const results = await Promise.all( + chunk.map(async (url) => { + const paddedIndex = String(index).padStart(4, "0"); + const destPath = resolve(classDir, `img_${paddedIndex}.jpg`); + + const success = await downloadImage(url, destPath); + await sleep(DOWNLOAD_DELAY); + return { success, index: index++ }; + }), + ); + + for (const r of results) { + if (r.success) downloaded++; + else failed++; + } + } + + return { downloaded, failed, lastIndex: index }; +} + +// ─── Progress Tracking ────────────────────────────────────────────────────── + +function loadProgress(): Progress { + if (!existsSync(PROGRESS_FILE)) { + return { lastUpdated: new Date().toISOString(), classes: {} }; + } + return JSON.parse(readFileSync(PROGRESS_FILE, "utf-8")) as Progress; +} + +function saveProgress(progress: Progress): void { + progress.lastUpdated = new Date().toISOString(); + writeFileSync(PROGRESS_FILE, JSON.stringify(progress, null, 2)); +} + +function getClassProgress(progress: Progress, classId: string): ClassProgress { + if (!progress.classes[classId]) { + progress.classes[classId] = { + count: 0, + downloaded: 0, + failed: 0, + skipped: 0, + seenUrls: [], + exhausted: false, + }; + } + return progress.classes[classId]; +} + +// ─── Search Query Building ────────────────────────────────────────────────── + +function buildSearchQueries(disease: DiseaseSeed, plant: PlantSeed | null): string[] { + const name = disease.name; + const plantName = plant?.commonName || disease.plantId; + + return [ + `${name} ${plantName} leaf disease`, + `${plantName} ${name} symptoms`, + `${name} plant disease`, + `${plantName} diseased leaf`, + ]; +} + +function buildHealthyQueries(plant: PlantSeed): string[] { + return [ + `healthy ${plant.commonName} leaf`, + `${plant.commonName} leaf closeup`, + `healthy ${plant.commonName} plant`, + `${plant.commonName} foliage`, + ]; +} + +// ─── Dataset Collection ───────────────────────────────────────────────────── + +async function collectClassImages( + classId: string, + queries: string[], + target: number, + progress: Progress, + classDir: string, +): Promise { + const cp = getClassProgress(progress, classId); + const seenUrls = new Set(cp.seenUrls); + + if (cp.count >= target) { + console.log(` ✓ Already have ${cp.count}/${target} images`); + return; + } + + if (cp.exhausted) { + console.log(` ✓ Already exhausted search results (${cp.count}/${target} images)`); + return; + } + + mkdirSync(classDir, { recursive: true }); + + const totalUrls: string[] = []; + let exhausted = false; + + // Search with each query until we hit the target + for (const query of queries) { + if (totalUrls.length >= target) break; + + console.log(` Searching: "${query}"...`); + const result = await collectImages(query, target - totalUrls.length, seenUrls); + + totalUrls.push(...result.urls); + cp.seenUrls = Array.from(seenUrls); + + if (result.exhausted) { + exhausted = true; + } + + if (totalUrls.length >= target) break; + } + + if (totalUrls.length === 0) { + cp.exhausted = exhausted; + saveProgress(progress); + console.log(` ✗ No images found for "${classId}"`); + return; + } + + console.log(` Found ${totalUrls.length} unique image URLs. Downloading...`); + + // Download the images + const { downloaded, failed } = await downloadBatch(totalUrls, classDir, cp.count); + + cp.count += downloaded; + cp.downloaded += downloaded; + cp.failed += failed; + cp.exhausted = exhausted; + + saveProgress(progress); + + const pct = Math.round((cp.count / target) * 100); + console.log( + ` ${downloaded > 0 ? "✓" : "✗"} Got ${downloaded} images (${failed} failed). Total: ${cp.count}/${target} (${pct}%)`, + ); +} + +// ─── Main ─────────────────────────────────────────────────────────────────── + +async function main() { + console.log("=".repeat(60)); + console.log("PLANT DISEASE DATASET COLLECTOR"); + console.log("=".repeat(60)); + + // Load knowledge base + const diseases = JSON.parse(readFileSync(DISEASES_JSON, "utf-8")) as DiseaseSeed[]; + const plants = JSON.parse(readFileSync(PLANTS_JSON, "utf-8")) as PlantSeed[]; + + const plantMap = new Map(); + for (const p of plants) { + plantMap.set(p.id, p); + } + + console.log(`\nLoaded ${diseases.length} diseases, ${plants.length} plants`); + console.log( + `Target: ${TARGET_PER_DISEASE} images/disease (×${diseases.length} = ${diseases.length * TARGET_PER_DISEASE})`, + ); + console.log(`Target: ${TARGET_HEALTHY} images for "healthy" class`); + console.log(`Output: ${DATASET_DIR}/`); + console.log(""); + + // Load progress + mkdirSync(DATASET_DIR, { recursive: true }); + const progress = loadProgress(); + + const startTime = Date.now(); + + // ── Phase 1: Disease classes ────────────────────────────────────────────── + + console.log("─".repeat(60)); + console.log("PHASE 1: Disease Images"); + console.log("─".repeat(60)); + + for (let i = 0; i < diseases.length; i++) { + const disease = diseases[i]; + const plant = plantMap.get(disease.plantId) ?? null; + const classDir = resolve(DATASET_DIR, disease.id); + const queries = buildSearchQueries(disease, plant); + + const pct = Math.round((i / diseases.length) * 100); + console.log(`\n[${i + 1}/${diseases.length}] (${pct}%) ${disease.name} (${disease.id})`); + + await collectClassImages(disease.id, queries, TARGET_PER_DISEASE, progress, classDir); + } + + // ── Phase 2: Healthy class ──────────────────────────────────────────────── + + console.log("\n" + "─".repeat(60)); + console.log("PHASE 2: Healthy Plant Images"); + console.log("─".repeat(60)); + + const healthyDir = resolve(DATASET_DIR, HEALTHY_CLASS); + const healthyCp = getClassProgress(progress, HEALTHY_CLASS); + const healthySeen = new Set(healthyCp.seenUrls); + + if (healthyCp.count >= TARGET_HEALTHY) { + console.log(`\n ✓ Already have ${healthyCp.count}/${TARGET_HEALTHY} healthy images`); + } else { + // Build a pool of healthy plant queries + const allHealthyQueries: string[] = []; + for (const plant of plants) { + allHealthyQueries.push(...buildHealthyQueries(plant)); + } + + const totalHealthyUrls: string[] = []; + let healthyExhausted = false; + + for (const query of allHealthyQueries) { + if (totalHealthyUrls.length >= TARGET_HEALTHY) break; + if (healthyExhausted) break; + + console.log(`\n Searching: "${query}"...`); + const result = await collectImages( + query, + TARGET_HEALTHY - totalHealthyUrls.length, + healthySeen, + ); + + totalHealthyUrls.push(...result.urls); + + if (result.exhausted) { + healthyExhausted = true; + } + } + + healthyCp.seenUrls = Array.from(healthySeen); + + if (totalHealthyUrls.length > 0) { + console.log(`\n Found ${totalHealthyUrls.length} healthy image URLs. Downloading...`); + const { downloaded, failed } = await downloadBatch( + totalHealthyUrls, + healthyDir, + healthyCp.count, + ); + + healthyCp.count += downloaded; + healthyCp.downloaded += downloaded; + healthyCp.failed += failed; + healthyCp.exhausted = healthyExhausted; + + const pct = Math.round((healthyCp.count / TARGET_HEALTHY) * 100); + console.log( + ` Got ${downloaded} images (${failed} failed). Total: ${healthyCp.count}/${TARGET_HEALTHY} (${pct}%)`, + ); + } else { + healthyCp.exhausted = true; + console.log(` ✗ No healthy images found`); + } + + saveProgress(progress); + } + + // ── Summary ──────────────────────────────────────────────────────────────── + + const elapsed = Math.round((Date.now() - startTime) / 1000); + const mins = Math.floor(elapsed / 60); + const secs = elapsed % 60; + + let totalDownloaded = 0; + let totalFailed = 0; + let totalTarget = 0; + + for (const [classId, cp] of Object.entries(progress.classes)) { + totalDownloaded += cp.downloaded || 0; + totalFailed += cp.failed || 0; + totalTarget += classId === HEALTHY_CLASS ? TARGET_HEALTHY : TARGET_PER_DISEASE; + } + + const totalSize = await getDatasetSize(); + const sizeGb = (totalSize / (1024 * 1024 * 1024)).toFixed(2); + + console.log("\n" + "=".repeat(60)); + console.log("COMPLETE"); + console.log("=".repeat(60)); + console.log(` Time: ${mins}m ${secs}s`); + console.log(` Downloaded: ${totalDownloaded} images`); + console.log(` Failed: ${totalFailed} images`); + console.log(` Target: ${totalTarget} images`); + console.log(` Dataset size: ${sizeGb} GB`); + console.log(` Dataset location: ${DATASET_DIR}/`); + console.log(""); + console.log("Next steps:"); + console.log(" 1. Run the fine-tuning script to train on this dataset"); + console.log(" 2. The fine-tuning script will resize to 160×160 and augment"); + console.log("=".repeat(60)); +} + +/** + * Calculate total size of the dataset directory. + */ +async function getDatasetSize(): Promise { + let total = 0; + if (!existsSync(DATASET_DIR)) return 0; + + const entries = readdirSync(DATASET_DIR, { withFileTypes: true }); + + for (const entry of entries) { + if (!entry.name.startsWith(".")) { + const fullPath = resolve(DATASET_DIR, entry.name); + if (entry.isDirectory()) { + total += dirSize(fullPath); + } + } + } + + return total; +} + +function dirSize(dirPath: string): number { + let total = 0; + try { + const entries = readdirSync(dirPath, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = join(dirPath, entry.name); + if (entry.isFile()) { + total += statSync(fullPath).size; + } else if (entry.isDirectory()) { + total += dirSize(fullPath); + } + } + } catch { + // skip errors + } + return total; +} + +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +main().catch((err) => { + console.error("Fatal error:", err); + process.exit(1); +}); diff --git a/apps/web/src/lib/image-processing.test.ts b/apps/web/src/lib/image-processing.test.ts index 3011ec4..e572b45 100644 --- a/apps/web/src/lib/image-processing.test.ts +++ b/apps/web/src/lib/image-processing.test.ts @@ -173,14 +173,14 @@ describe("imageToTensor", () => { describe("tensorToBase64 / base64ToTensor", () => { it("round-trips tensor data correctly", () => { - const imageData = createMockImageData(224, 224, 100, 150, 200); + const imageData = createMockImageData(160, 160, 100, 150, 200); const original = imageToTensor(imageData); const base64 = tensorToBase64(original); const decoded = base64ToTensor(base64); expect(decoded.tensor.length).toBe(original.length); - expect(decoded.shape).toEqual([3, 224, 224]); + expect(decoded.shape).toEqual([3, 160, 160]); // Check a few values match for (let i = 0; i < 10; i++) { @@ -197,9 +197,9 @@ describe("tensorToBase64 / base64ToTensor", () => { }); describe("getTensorShape", () => { - it("returns [1, 3, 224, 224] by default", () => { + it("returns [1, 3, 160, 160] by default", () => { const shape = getTensorShape(); - expect(shape).toEqual([1, 3, 224, 224]); + expect(shape).toEqual([1, 3, 160, 160]); }); it("returns NCHW layout", () => { @@ -207,8 +207,8 @@ describe("getTensorShape", () => { expect(shape.length).toBe(4); expect(shape[0]).toBe(1); // batch expect(shape[1]).toBe(3); // channels - expect(shape[2]).toBe(224); // height - expect(shape[3]).toBe(224); // width + expect(shape[2]).toBe(160); // height (model input size) + expect(shape[3]).toBe(160); // width (model input size) }); }); diff --git a/apps/web/src/lib/image-processing.ts b/apps/web/src/lib/image-processing.ts index fcab83f..f37614d 100644 --- a/apps/web/src/lib/image-processing.ts +++ b/apps/web/src/lib/image-processing.ts @@ -17,7 +17,7 @@ const DEFAULT_MODEL_SIZE = 160; const DEFAULT_MEAN = [0.485, 0.456, 0.406] as const; // ImageNet RGB means -const DEFAULT_STD = [0.229, 0.224, 0.225] as const; // ImageNet RGB stds +const DEFAULT_STD = [0.229, 0.224, 0.225] as const; // ImageNet RGB stds function getConfig(): { size: number; @@ -48,12 +48,7 @@ export const MAX_FILE_SIZE = 10 * 1024 * 1024; export const MIN_DIMENSION = 150; /** Allowed MIME types */ -export const ALLOWED_MIME_TYPES = [ - "image/png", - "image/jpeg", - "image/jpg", - "image/webp", -] as const; +export const ALLOWED_MIME_TYPES = ["image/png", "image/jpeg", "image/jpg", "image/webp"] as const; export type AllowedMimeType = (typeof ALLOWED_MIME_TYPES)[number]; @@ -66,9 +61,7 @@ export const MAX_UPLOADS = 100; * Validate that a file is an acceptable image for upload. * Returns `{ ok: true }` or `{ ok: false, error: string }`. */ -export function validateImageFile(file: File): - | { ok: true } - | { ok: false; error: string } { +export function validateImageFile(file: File): { ok: true } | { ok: false; error: string } { // MIME type check if (!ALLOWED_MIME_TYPES.includes(file.type as AllowedMimeType)) { return { @@ -127,10 +120,7 @@ export function validateImageDimensions( * @param size - Target dimension (square). Defaults to IMAGE_MODEL_SIZE env or 224. * @returns ImageData at exactly `size × size` */ -export async function resizeImage( - file: File, - size: number = getConfig().size, -): Promise { +export async function resizeImage(file: File, size: number = getConfig().size): Promise { return new Promise((resolve, reject) => { const img = new Image(); img.onload = () => { @@ -193,8 +183,7 @@ export function imageToTensor(imageData: ImageData): Float32Array { // Normalize with ImageNet mean/std for (let c = 0; c < 3; c++) { - const channel = - c === 0 ? rChannel : c === 1 ? gChannel : bChannel; + const channel = c === 0 ? rChannel : c === 1 ? gChannel : bChannel; const m = mean[c]; const s = std[c]; for (let i = 0; i < totalPixels; i++) { @@ -253,5 +242,3 @@ export function base64ToTensor(base64: string): { shape: envelope.shape as [number, number, number], }; } - - diff --git a/apps/web/src/lib/ml/inference.test.ts b/apps/web/src/lib/ml/inference.test.ts index 88083a2..f6b7d1c 100644 --- a/apps/web/src/lib/ml/inference.test.ts +++ b/apps/web/src/lib/ml/inference.test.ts @@ -97,7 +97,7 @@ describe("createZeroTensor", () => { it("all values are zero", () => { const tensor = createZeroTensor(); - expect(tensor.every(v => v === 0)).toBe(true); + expect(tensor.every((v) => v === 0)).toBe(true); }); }); @@ -114,12 +114,12 @@ describe("createRandomTensor", () => { it("all values are finite", () => { const tensor = createRandomTensor(); - expect(tensor.every(v => Number.isFinite(v))).toBe(true); + expect(tensor.every((v) => Number.isFinite(v))).toBe(true); }); it("produces varied values", () => { const tensor = createRandomTensor(); - const uniqueValues = new Set(tensor.map(v => v.toFixed(4))); + const uniqueValues = new Set(tensor.map((v) => v.toFixed(4))); expect(uniqueValues.size).toBeGreaterThan(100); }); @@ -172,7 +172,7 @@ describe("runInference", () => { const result = await runInference(tensor); for (let i = 0; i < result.predictions.length - 1; i++) { expect(result.predictions[i].probability).toBeGreaterThanOrEqual( - result.predictions[i + 1].probability + result.predictions[i + 1].probability, ); } }, 10000); diff --git a/apps/web/src/lib/ml/inference.ts b/apps/web/src/lib/ml/inference.ts index a6fbbbc..5eebd09 100644 --- a/apps/web/src/lib/ml/inference.ts +++ b/apps/web/src/lib/ml/inference.ts @@ -69,9 +69,7 @@ export async function runInference( */ export function validateInput(tensor: Float32Array): void { if (!(tensor instanceof Float32Array)) { - throw new Error( - `Expected Float32Array input, got ${typeof tensor}`, - ); + throw new Error(`Expected Float32Array input, got ${typeof tensor}`); } if (tensor.length !== INPUT_SIZE) { @@ -84,9 +82,7 @@ export function validateInput(tensor: Float32Array): void { // Check for NaN/Infinity values for (let i = 0; i < tensor.length; i++) { if (!Number.isFinite(tensor[i])) { - throw new Error( - `Tensor contains non-finite value at index ${i}: ${tensor[i]}`, - ); + throw new Error(`Tensor contains non-finite value at index ${i}: ${tensor[i]}`); } } } diff --git a/apps/web/src/lib/ml/labels.test.ts b/apps/web/src/lib/ml/labels.test.ts index dbe3b0c..4cd38f6 100644 --- a/apps/web/src/lib/ml/labels.test.ts +++ b/apps/web/src/lib/ml/labels.test.ts @@ -1,17 +1,21 @@ /** * Unit tests for lib/ml/labels.ts * - * Tests: - * - INDEX_TO_DISEASE_ID maps index 0 to "healthy" - * - INDEX_TO_DISEASE_ID maps last index to "unknown" - * - INDEX_TO_DISEASE_ID maps intermediate indices to disease IDs - * - DISEASE_ID_TO_INDEX is inverse of INDEX_TO_DISEASE_ID - * - getDiseaseIdForIndex returns "unknown" for out-of-range - * - getIndexForDiseaseId returns -1 for unknown ID - * - isRealDisease correctly classifies healthy/unknown vs real diseases - * - getAllDiseaseIds returns all disease IDs from knowledge base - * - NUM_CLASSES equals expected count (diseases + healthy + unknown) - * - Bidirectional mapping consistency + * The model has 38 PlantVillage classes. Some map to the app's + * knowledge base disease IDs, others map to "unknown". + * + * Known mappings: + * - indices 3, 4, 6, 10, 14, 17, 19, 22, 23, 24, 27, 37 → "healthy" + * - index 20 (Potato___Early_blight) → "early-blight" + * - index 21 (Potato___Late_blight) → "late-blight" + * - index 25 (Squash___Powdery_mildew) → "squash-powdery-mildew" + * - index 26 (Strawberry___Leaf_scorch) → "strawberry-leaf-scorch" + * - index 28 (Tomato___Bacterial_spot) → "bacterial-leaf-spot-tomato" + * - index 29 (Tomato___Early_blight) → "early-blight" (duplicate) + * - index 30 (Tomato___Late_blight) → "late-blight" (duplicate) + * - index 32 (Tomato___Septoria_leaf_spot) → "septoria-leaf-spot" + * - index 37 (Tomato___healthy) → "healthy" + * - all others → "unknown" */ import { describe, it, expect } from "vitest"; @@ -23,143 +27,105 @@ import { isRealDisease, getAllDiseaseIds, NUM_CLASSES, - HEALTHY_INDEX, - FIRST_DISEASE_INDEX, - UNKNOWN_INDEX, + getPlantVillageClassName, } from "@/lib/ml/labels"; -import rawDiseases from "@/data/diseases.json"; -import type { Disease } from "@/lib/types"; - -const diseases: Disease[] = rawDiseases as Disease[]; describe("Constants", () => { - it("HEALTHY_INDEX is 0", () => { - expect(HEALTHY_INDEX).toBe(0); + it("NUM_CLASSES is 38 (PlantVillage)", () => { + expect(NUM_CLASSES).toBe(38); }); - it("FIRST_DISEASE_INDEX is 1", () => { - expect(FIRST_DISEASE_INDEX).toBe(1); - }); - - it("UNKNOWN_INDEX is 1 + number of diseases", () => { - expect(UNKNOWN_INDEX).toBe(1 + diseases.length); - }); - - it("NUM_CLASSES is UNKNOWN_INDEX + 1", () => { - expect(NUM_CLASSES).toBe(UNKNOWN_INDEX + 1); - }); - - it("NUM_CLASSES equals diseases.length + 2 (healthy + unknown)", () => { - expect(NUM_CLASSES).toBe(diseases.length + 2); + it("all 38 indices are mapped", () => { + const keys = Object.keys(INDEX_TO_DISEASE_ID).map(Number); + expect(keys.length).toBe(38); + for (let i = 0; i < 38; i++) { + expect(keys).toContain(i); + } }); }); -describe("INDEX_TO_DISEASE_ID", () => { - it("maps index 0 to 'healthy'", () => { - expect(INDEX_TO_DISEASE_ID[0]).toBe("healthy"); - }); +describe("INDEX_TO_DISEASE_ID — healthy indices", () => { + const healthyIndices = [3, 4, 6, 10, 14, 17, 19, 22, 23, 24, 27, 37]; - it("maps last index to 'unknown'", () => { - expect(INDEX_TO_DISEASE_ID[NUM_CLASSES - 1]).toBe("unknown"); - }); + for (const idx of healthyIndices) { + it(`index ${idx} maps to "healthy"`, () => { + expect(INDEX_TO_DISEASE_ID[idx]).toBe("healthy"); + }); + } +}); - it("maps intermediate indices to disease IDs", () => { - // Index 1 should be the first disease - expect(INDEX_TO_DISEASE_ID[1]).toBe(diseases[0].id); - // Index 2 should be the second disease - expect(INDEX_TO_DISEASE_ID[2]).toBe(diseases[1].id); - // Last disease index - expect(INDEX_TO_DISEASE_ID[diseases.length]).toBe(diseases[diseases.length - 1].id); - }); +describe("INDEX_TO_DISEASE_ID — known disease mappings", () => { + const cases: Array<{ index: number; expected: string; name: string }> = [ + { index: 20, expected: "early-blight", name: "Potato___Early_blight" }, + { index: 21, expected: "late-blight", name: "Potato___Late_blight" }, + { index: 25, expected: "squash-powdery-mildew", name: "Squash___Powdery_mildew" }, + { index: 26, expected: "strawberry-leaf-scorch", name: "Strawberry___Leaf_scorch" }, + { index: 28, expected: "bacterial-leaf-spot-tomato", name: "Tomato___Bacterial_spot" }, + { index: 29, expected: "early-blight", name: "Tomato___Early_blight" }, + { index: 30, expected: "late-blight", name: "Tomato___Late_blight" }, + { index: 32, expected: "septoria-leaf-spot", name: "Tomato___Septoria_leaf_spot" }, + ]; - it("has exactly NUM_CLASSES entries", () => { - const keys = Object.keys(INDEX_TO_DISEASE_ID); - expect(keys.length).toBe(NUM_CLASSES); - }); + for (const { index, expected, name } of cases) { + it(`index ${index} (${name}) maps to "${expected}"`, () => { + expect(INDEX_TO_DISEASE_ID[index]).toBe(expected); + }); + } +}); - it("all mapped IDs are valid strings", () => { - for (const id of Object.values(INDEX_TO_DISEASE_ID)) { - expect(typeof id).toBe("string"); - expect(id.length).toBeGreaterThan(0); - } - }); +describe("INDEX_TO_DISEASE_ID — unknown (unmapped) indices", () => { + const unknownIndices = [0, 1, 2, 5, 7, 8, 9, 11, 12, 13, 15, 16, 18, 31, 33, 34, 35, 36]; + + for (const idx of unknownIndices) { + it(`index ${idx} maps to "unknown"`, () => { + expect(INDEX_TO_DISEASE_ID[idx]).toBe("unknown"); + }); + } }); describe("DISEASE_ID_TO_INDEX", () => { - it("maps 'healthy' to index 0", () => { - expect(DISEASE_ID_TO_INDEX["healthy"]).toBe(0); + it("maps 'early-blight' to first occurrence (index 20)", () => { + expect(DISEASE_ID_TO_INDEX["early-blight"]).toBe(20); }); - it("maps 'unknown' to last index", () => { - expect(DISEASE_ID_TO_INDEX["unknown"]).toBe(NUM_CLASSES - 1); + it("maps 'late-blight' to first occurrence (index 21)", () => { + expect(DISEASE_ID_TO_INDEX["late-blight"]).toBe(21); }); - it("maps disease IDs to correct indices", () => { - for (let i = 0; i < diseases.length; i++) { - expect(DISEASE_ID_TO_INDEX[diseases[i].id]).toBe(FIRST_DISEASE_INDEX + i); - } + it("maps 'septoria-leaf-spot' to index 32", () => { + expect(DISEASE_ID_TO_INDEX["septoria-leaf-spot"]).toBe(32); }); - it("has exactly NUM_CLASSES entries", () => { - const keys = Object.keys(DISEASE_ID_TO_INDEX); - expect(keys.length).toBe(NUM_CLASSES); + it("maps 'healthy' to index 3 (first healthy index)", () => { + expect(DISEASE_ID_TO_INDEX["healthy"]).toBe(3); }); }); describe("Bidirectional mapping", () => { - it("INDEX_TO_DISEASE_ID and DISEASE_ID_TO_INDEX are inverses", () => { - for (const [idxStr, id] of Object.entries(INDEX_TO_DISEASE_ID)) { - const idx = parseInt(idxStr); - expect(DISEASE_ID_TO_INDEX[id]).toBe(idx); - } - }); - - it("round-trips for all disease IDs", () => { - for (const [id, idx] of Object.entries(DISEASE_ID_TO_INDEX)) { - expect(INDEX_TO_DISEASE_ID[idx]).toBe(id); - } - }); - - it("round-trips for all indices", () => { + it("every index round-trips correctly", () => { for (let i = 0; i < NUM_CLASSES; i++) { const id = INDEX_TO_DISEASE_ID[i]; - expect(DISEASE_ID_TO_INDEX[id]).toBe(i); + const idx = DISEASE_ID_TO_INDEX[id]; + expect(INDEX_TO_DISEASE_ID[idx]).toBe(id); } }); }); describe("getDiseaseIdForIndex", () => { - it("returns 'healthy' for index 0", () => { - expect(getDiseaseIdForIndex(0)).toBe("healthy"); - }); - - it("returns disease ID for valid disease index", () => { - expect(getDiseaseIdForIndex(1)).toBe(diseases[0].id); - }); - it("returns 'unknown' for out-of-range positive index", () => { - expect(getDiseaseIdForIndex(1000)).toBe("unknown"); + expect(getDiseaseIdForIndex(100)).toBe("unknown"); }); it("returns 'unknown' for negative index", () => { expect(getDiseaseIdForIndex(-1)).toBe("unknown"); }); - it("returns 'unknown' for index past NUM_CLASSES", () => { - expect(getDiseaseIdForIndex(NUM_CLASSES + 10)).toBe("unknown"); + it("returns correct ID for valid index", () => { + expect(getDiseaseIdForIndex(20)).toBe("early-blight"); }); }); describe("getIndexForDiseaseId", () => { - it("returns 0 for 'healthy'", () => { - expect(getIndexForDiseaseId("healthy")).toBe(0); - }); - - it("returns correct index for known disease", () => { - const idx = getIndexForDiseaseId(diseases[0].id); - expect(idx).toBe(1); - }); - it("returns -1 for unknown disease ID", () => { expect(getIndexForDiseaseId("nonexistent-disease")).toBe(-1); }); @@ -169,9 +135,7 @@ describe("getIndexForDiseaseId", () => { }); it("is case-insensitive", () => { - const lowerIdx = getIndexForDiseaseId(diseases[0].id); - const upperIdx = getIndexForDiseaseId(diseases[0].id.toUpperCase()); - expect(upperIdx).toBe(lowerIdx); + expect(getIndexForDiseaseId("EARLY-BLIGHT")).toBe(20); }); }); @@ -184,10 +148,9 @@ describe("isRealDisease", () => { expect(isRealDisease("unknown")).toBe(false); }); - it("returns true for actual disease IDs", () => { - for (const disease of diseases) { - expect(isRealDisease(disease.id)).toBe(true); - } + it("returns true for known disease IDs", () => { + expect(isRealDisease("early-blight")).toBe(true); + expect(isRealDisease("septoria-leaf-spot")).toBe(true); }); it("returns true for arbitrary non-special strings", () => { @@ -195,27 +158,37 @@ describe("isRealDisease", () => { }); }); +describe("getPlantVillageClassName", () => { + it("returns correct class name for tomato healthy", () => { + expect(getPlantVillageClassName(37)).toBe("Tomato___healthy"); + }); + + it("returns correct class name for potato early blight", () => { + expect(getPlantVillageClassName(20)).toBe("Potato___Early_blight"); + }); + + it("returns 'unknown' for out-of-range index", () => { + expect(getPlantVillageClassName(100)).toBe("unknown"); + }); +}); + describe("getAllDiseaseIds", () => { - it("returns array of all disease IDs", () => { + it("returns only mapped disease IDs", () => { const ids = getAllDiseaseIds(); - expect(ids.length).toBe(diseases.length); + expect(ids).toContain("early-blight"); + expect(ids).toContain("late-blight"); + expect(ids).toContain("squash-powdery-mildew"); + expect(ids).toContain("strawberry-leaf-scorch"); + expect(ids).toContain("bacterial-leaf-spot-tomato"); + expect(ids).toContain("septoria-leaf-spot"); }); it("excludes 'healthy'", () => { - const ids = getAllDiseaseIds(); - expect(ids).not.toContain("healthy"); + expect(getAllDiseaseIds()).not.toContain("healthy"); }); it("excludes 'unknown'", () => { - const ids = getAllDiseaseIds(); - expect(ids).not.toContain("unknown"); - }); - - it("includes all disease IDs from knowledge base", () => { - const ids = getAllDiseaseIds(); - for (const disease of diseases) { - expect(ids).toContain(disease.id); - } + expect(getAllDiseaseIds()).not.toContain("unknown"); }); it("has no duplicates", () => { diff --git a/apps/web/src/lib/ml/labels.ts b/apps/web/src/lib/ml/labels.ts index dae7dfa..bd1f97c 100644 --- a/apps/web/src/lib/ml/labels.ts +++ b/apps/web/src/lib/ml/labels.ts @@ -1,74 +1,197 @@ /** * Class label mapping for the plant disease classifier model. * - * Maps model output index → disease ID string. - * The model has classes for each disease in the knowledge base, - * plus "healthy" and "unknown" catch-all classes. + * This model is a MobileNetV2 trained on the PlantVillage dataset + * with 38 classes (14 crops × diseases/healthy). * - * Model output shape: [1, NUM_CLASSES] where NUM_CLASSES = 95 - * (93 diseases + "healthy" + "unknown") + * Model output shape: [1, NUM_CLASSES] where NUM_CLASSES = 38 * - * Index layout: - * 0 → "healthy" - * 1–93 → disease IDs (order matches diseases.json) - * 94 → "unknown" + * Index layout (from labels_pv_original.json): + * 0 → Apple___Apple_scab + * 1 → Apple___Black_rot + * 2 → Apple___Cedar_apple_rust + * 3 → Apple___healthy + * 4 → Blueberry___healthy + * 5 → Cherry_(including_sour)___Powdery_mildew + * 6 → Cherry_(including_sour)___healthy + * 7 → Corn_(maize)___Cercospora_leaf_spot Gray_leaf_spot + * 8 → Corn_(maize)___Common_rust_ + * 9 → Corn_(maize)___Northern_Leaf_Blight + * 10 → Corn_(maize)___healthy + * 11 → Grape___Black_rot + * 12 → Grape___Esca_(Black_Measles) + * 13 → Grape___Leaf_blight_(Isariopsis_Leaf_Spot) + * 14 → Grape___healthy + * 15 → Orange___Haunglongbing_(Citrus_greening) + * 16 → Peach___Bacterial_spot + * 17 → Peach___healthy + * 18 → Pepper,_bell___Bacterial_spot + * 19 → Pepper,_bell___healthy + * 20 → Potato___Early_blight + * 21 → Potato___Late_blight + * 22 → Potato___healthy + * 23 → Raspberry___healthy + * 24 → Soybean___healthy + * 25 → Squash___Powdery_mildew + * 26 → Strawberry___Leaf_scorch + * 27 → Strawberry___healthy + * 28 → Tomato___Bacterial_spot + * 29 → Tomato___Early_blight + * 30 → Tomato___Late_blight + * 31 → Tomato___Leaf_Mold + * 32 → Tomato___Septoria_leaf_spot + * 33 → Tomato___Spider_mites Two-spotted_spider_mite + * 34 → Tomato___Target_Spot + * 35 → Tomato___Tomato_Yellow_Leaf_Curl_Virus + * 36 → Tomato___Tomato_mosaic_virus + * 37 → Tomato___healthy + * + * Some PlantVillage classes overlap with this app's knowledge base. + * Exact class name → disease ID mappings: + * Potato___Early_blight → "early-blight" + * Potato___Late_blight → "late-blight" + * Squash___Powdery_mildew → "squash-powdery-mildew" + * Strawberry___Leaf_scorch → "strawberry-leaf-scorch" + * Tomato___Bacterial_spot → "bacterial-leaf-spot-tomato" + * Tomato___Early_blight → "early-blight" + * Tomato___Late_blight → "late-blight" + * Tomato___Septoria_leaf_spot → "septoria-leaf-spot" + * All other classes map to "unknown" and are filtered out during enrichment. + * + * After fine-tuning to the app's 93 disease classes, this file will be + * rewritten to match the new model's output layer. */ -import rawDiseases from "@/data/diseases.json"; -import type { Disease } from "@/lib/types"; +// ─── PlantVillage class names (in model output order) ──────────────────── -const diseases: Disease[] = rawDiseases as Disease[]; +const PLANTVILLAGE_CLASSES: string[] = [ + "Apple___Apple_scab", + "Apple___Black_rot", + "Apple___Cedar_apple_rust", + "Apple___healthy", + "Blueberry___healthy", + "Cherry_(including_sour)___Powdery_mildew", + "Cherry_(including_sour)___healthy", + "Corn_(maize)___Cercospora_leaf_spot Gray_leaf_spot", + "Corn_(maize)___Common_rust_", + "Corn_(maize)___Northern_Leaf_Blight", + "Corn_(maize)___healthy", + "Grape___Black_rot", + "Grape___Esca_(Black_Measles)", + "Grape___Leaf_blight_(Isariopsis_Leaf_Spot)", + "Grape___healthy", + "Orange___Haunglongbing_(Citrus_greening)", + "Peach___Bacterial_spot", + "Peach___healthy", + "Pepper,_bell___Bacterial_spot", + "Pepper,_bell___healthy", + "Potato___Early_blight", + "Potato___Late_blight", + "Potato___healthy", + "Raspberry___healthy", + "Soybean___healthy", + "Squash___Powdery_mildew", + "Strawberry___Leaf_scorch", + "Strawberry___healthy", + "Tomato___Bacterial_spot", + "Tomato___Early_blight", + "Tomato___Late_blight", + "Tomato___Leaf_Mold", + "Tomato___Septoria_leaf_spot", + "Tomato___Spider_mites Two-spotted_spider_mite", + "Tomato___Target_Spot", + "Tomato___Tomato_Yellow_Leaf_Curl_Virus", + "Tomato___Tomato_mosaic_virus", + "Tomato___healthy", +] as const; -// ─── Constants ─────────────────────────────────────────────────────────────── - -/** Index for the "healthy" class */ -export const HEALTHY_INDEX = 0; - -/** First index for actual disease classes */ -export const FIRST_DISEASE_INDEX = 1; - -/** Index for the "unknown" catch-all class */ -export const UNKNOWN_INDEX = 1 + diseases.length; - -/** Total number of output classes */ -export const NUM_CLASSES = UNKNOWN_INDEX + 1; - -// ─── Index → Disease ID mapping ────────────────────────────────────────────── +// ─── PlantVillage → App disease ID mapping ────────────────────────────── /** - * Map from model output index to disease ID string. - * Index 0 = "healthy", indices 1..N = disease IDs, last = "unknown". + * Maps PlantVillage class names (in the form "Plant___Disease") to + * this app's disease IDs. Unmapped classes resolve to "unknown". + */ +function plantVillageNameToDiseaseId(pvName: string): string { + const parts = pvName.split("___"); + if (parts.length !== 2) { + return "unknown"; + } + + const disease = parts[1]; + + // Detect "healthy" variants + if (disease === "healthy") { + return "healthy"; + } + + // Map exact PlantVillage class names to our disease IDs. + // Only map classes where we're confident the correspondence holds. + const exactMap: Record = { + Squash___Powdery_mildew: "squash-powdery-mildew", + Strawberry___Leaf_scorch: "strawberry-leaf-scorch", + Potato___Early_blight: "early-blight", + Potato___Late_blight: "late-blight", + Tomato___Bacterial_spot: "bacterial-leaf-spot-tomato", + Tomato___Early_blight: "early-blight", + Tomato___Late_blight: "late-blight", + Tomato___Septoria_leaf_spot: "septoria-leaf-spot", + }; + + return exactMap[pvName] ?? "unknown"; +} + +// ─── Constants ────────────────────────────────────────────────────────── + +/** Total number of model output classes */ +export const NUM_CLASSES = PLANTVILLAGE_CLASSES.length; // 38 + +/** Index for the "healthy" class — multiple PV indices map to this */ +export const HEALTHY_INDEX = 0; // First PV healthy class, others also map to this string + +/** First disease index (unused in PV mapping, kept for compatibility) */ +export const FIRST_DISEASE_INDEX = 0; + +/** Index for the "unknown" catch-all — PV classes we can't map */ +export const UNKNOWN_INDEX = NUM_CLASSES - 1; // 37 (Tomato___healthy maps to "healthy", not unknown) + +// ─── Index → Disease ID mapping ───────────────────────────────────────── + +/** + * Map from model output index to app disease ID string. + * Built dynamically from PlantVillage class names. */ export const INDEX_TO_DISEASE_ID: Record = Object.freeze( (() => { const map: Record = {}; - map[HEALTHY_INDEX] = "healthy"; - for (let i = 0; i < diseases.length; i++) { - map[FIRST_DISEASE_INDEX + i] = diseases[i].id; + for (let i = 0; i < NUM_CLASSES; i++) { + map[i] = plantVillageNameToDiseaseId(PLANTVILLAGE_CLASSES[i]); } - map[UNKNOWN_INDEX] = "unknown"; return map; })(), ); -// ─── Disease ID → Index mapping ────────────────────────────────────────────── +// ─── Disease ID → Index mapping ───────────────────────────────────────── /** * Map from disease ID string to model output index. + * For duplicates (e.g., both potato and tomato "Early_blight" → "early-blight"), + * returns the first matching index. */ export const DISEASE_ID_TO_INDEX: Record = Object.freeze( (() => { const map: Record = {}; - map["healthy"] = HEALTHY_INDEX; - for (let i = 0; i < diseases.length; i++) { - map[diseases[i].id] = FIRST_DISEASE_INDEX + i; + for (let i = 0; i < NUM_CLASSES; i++) { + const id = INDEX_TO_DISEASE_ID[i]; + // First occurrence wins (potato before tomato for early/late blight) + if (map[id] === undefined) { + map[id] = i; + } } - map["unknown"] = UNKNOWN_INDEX; return map; })(), ); -// ─── Lookup helpers ────────────────────────────────────────────────────────── +// ─── Lookup helpers ───────────────────────────────────────────────────── /** * Get the disease ID for a given model output index. @@ -93,9 +216,22 @@ export function isRealDisease(diseaseId: string): boolean { return diseaseId !== "healthy" && diseaseId !== "unknown"; } +/** + * Get the PlantVillage display name for a given model output index. + */ +export function getPlantVillageClassName(index: number): string { + return PLANTVILLAGE_CLASSES[index] ?? "unknown"; +} + /** * Get all known disease IDs (excluding "healthy" and "unknown"). */ export function getAllDiseaseIds(): string[] { - return diseases.map((d) => d.id); + const ids = new Set(); + for (const id of Object.values(INDEX_TO_DISEASE_ID)) { + if (id !== "healthy" && id !== "unknown") { + ids.add(id); + } + } + return Array.from(ids); } diff --git a/apps/web/src/lib/ml/model-loader.ts b/apps/web/src/lib/ml/model-loader.ts index ba90e65..253a2f5 100644 --- a/apps/web/src/lib/ml/model-loader.ts +++ b/apps/web/src/lib/ml/model-loader.ts @@ -93,7 +93,10 @@ export async function getModel(): Promise { const model = await Promise.race([ loadingPromise, new Promise((_, reject) => - setTimeout(() => reject(new Error(`Model load timed out after ${MODEL_LOAD_TIMEOUT}ms`)), MODEL_LOAD_TIMEOUT), + setTimeout( + () => reject(new Error(`Model load timed out after ${MODEL_LOAD_TIMEOUT}ms`)), + MODEL_LOAD_TIMEOUT, + ), ), ]); @@ -172,6 +175,18 @@ async function tryLoadTFJS(): Promise { // eslint-disable-next-line @typescript-eslint/no-explicit-any let tf: any; + // Monkey-patch: add util.isNullOrUndefined for Node.js 26 compatibility. + // @tensorflow/tfjs-node references this function which was removed in Node 15+. + // eslint-disable-next-line @typescript-eslint/no-require-imports + const nodeUtil = require("util"); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if (typeof (nodeUtil as any).isNullOrUndefined !== "function") { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (nodeUtil as any).isNullOrUndefined = function (x: unknown): boolean { + return x === null || x === undefined; + }; + } + // Try tfjs-node first (server-side, uses native bindings). // Use dynamic strings so bundlers (Turbopack/webpack) don't trace these // as required dependencies — they are truly optional. @@ -197,7 +212,9 @@ async function tryLoadTFJS(): Promise { const startTime = performance.now(); // Reshape to [1, 3, 160, 160] NCHW → [1, 160, 160, 3] NHWC for TF.js - const inputTensor = tf.tensor4d(Array.from(tensor), [3, 160, 160]) + // Reshape NCHW flat array [3*160*160] → [3, 160, 160] → NHWC [1, 160, 160, 3] + const inputTensor = tf + .tensor3d(Array.from(tensor), [3, 160, 160]) .transpose([1, 2, 0]) .expandDims(0); @@ -352,7 +369,7 @@ function generateMockLogits(tensor: Float32Array): Float32Array { logits[topIndex] = 3.5; // Second highest - const secondIndex = (topIndex + Math.abs(hash % 10) + 1) % (numClasses - 1) + 1; + const secondIndex = ((topIndex + Math.abs(hash % 10) + 1) % (numClasses - 1)) + 1; logits[secondIndex] = 2.5; logits[numClasses - 1] = -2; // "unknown" gets low score