Token Classification
Transformers
ONNX
Safetensors
English
Japanese
Chinese
bert
anime
filename-parsing
Eval Results (legacy)
Instructions to use ModerRAS/AniFileBERT with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use ModerRAS/AniFileBERT with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("token-classification", model="ModerRAS/AniFileBERT")# pip install -U transformers accelerate # Load model directly from transformers import AutoTokenizer, AutoModelForTokenClassification tokenizer = AutoTokenizer.from_pretrained("ModerRAS/AniFileBERT") model = AutoModelForTokenClassification.from_pretrained("ModerRAS/AniFileBERT", device_map="auto") - Notebooks
- Google Colab
- Kaggle
Parallelize Pi RPC title-boundary reviews
Browse files- AGENTS.md +34 -0
- tools/rust_dmhy_template_apply/README.md +27 -0
- tools/rust_dmhy_template_apply/src/main.rs +199 -80
AGENTS.md
CHANGED
|
@@ -116,6 +116,40 @@ cargo run --release --manifest-path tools\schema_v2_synthetic_augment\Cargo.toml
|
|
| 116 |
--manifest data\schema_v2_synthetic_aug.manifest.json
|
| 117 |
```
|
| 118 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
Preferred synthetic follow-up training is a second stage from the best repaired
|
| 120 |
hard-focus checkpoint, not a replacement for hard-focus. Keep this path
|
| 121 |
Rust-cache-first: build one combined encoded cache from hard-focus JSONL plus
|
|
|
|
| 116 |
--manifest data\schema_v2_synthetic_aug.manifest.json
|
| 117 |
```
|
| 118 |
|
| 119 |
+
When auditing Rust DMHY title-boundary recipe decisions with an LLM, prefer the
|
| 120 |
+
Rust reviewer cache flow. The LM reviewer is optional evidence only; it must not
|
| 121 |
+
mutate recipes or training JSONL directly. Use Pi RPC for stronger model-backed
|
| 122 |
+
reviews, and keep the default deterministic Rust heuristic as the source of
|
| 123 |
+
truth until reviewed decisions are explicitly consumed by a separate Rust merge
|
| 124 |
+
step:
|
| 125 |
+
|
| 126 |
+
```powershell
|
| 127 |
+
cargo run --release --manifest-path tools\rust_dmhy_template_apply\Cargo.toml -- `
|
| 128 |
+
--review-title-boundaries-pi-rpc `
|
| 129 |
+
--recipes reports\dmhy_template_recipes.full_top5000.title_boundary_trial.seed.jsonl `
|
| 130 |
+
--title-boundary-decisions-output reports\dmhy_title_boundary_pi_rpc_decisions.title_boundary_trial.jsonl `
|
| 131 |
+
--title-boundary-min-similarity 0.25 `
|
| 132 |
+
--title-boundary-max-similarity 0.85 `
|
| 133 |
+
--threads 4
|
| 134 |
+
```
|
| 135 |
+
|
| 136 |
+
For reviewer calibration, run stratified samples before full review. Use
|
| 137 |
+
`--title-boundary-heuristic-contains` to inspect high-similar duplicate/alias
|
| 138 |
+
keeps, structural keeps, and path-boundary keeps that are normally skipped by
|
| 139 |
+
the ambiguous merge review:
|
| 140 |
+
|
| 141 |
+
```powershell
|
| 142 |
+
cargo run --release --manifest-path tools\rust_dmhy_template_apply\Cargo.toml -- `
|
| 143 |
+
--review-title-boundaries-pi-rpc `
|
| 144 |
+
--recipes reports\dmhy_template_recipes.full_top5000.title_boundary_trial.seed.jsonl `
|
| 145 |
+
--title-boundary-decisions-output reports\dmhy_title_boundary_pi_rpc_highsim_keep_sample.jsonl `
|
| 146 |
+
--title-boundary-heuristic-contains keep_similar_duplicate_or_alias `
|
| 147 |
+
--title-boundary-min-similarity 0.85 `
|
| 148 |
+
--title-boundary-max-similarity 1.0 `
|
| 149 |
+
--threads 4 `
|
| 150 |
+
--limit 20
|
| 151 |
+
```
|
| 152 |
+
|
| 153 |
Preferred synthetic follow-up training is a second stage from the best repaired
|
| 154 |
hard-focus checkpoint, not a replacement for hard-focus. Keep this path
|
| 155 |
Rust-cache-first: build one combined encoded cache from hard-focus JSONL plus
|
tools/rust_dmhy_template_apply/README.md
CHANGED
|
@@ -77,9 +77,36 @@ cargo run --release --manifest-path tools\rust_dmhy_template_apply\Cargo.toml --
|
|
| 77 |
--pi-rpc-model openai/gpt-5.5 `
|
| 78 |
--title-boundary-min-similarity 0.25 `
|
| 79 |
--title-boundary-max-similarity 0.85 `
|
|
|
|
| 80 |
--limit 50
|
| 81 |
```
|
| 82 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
Optional controls:
|
| 84 |
|
| 85 |
```powershell
|
|
|
|
| 77 |
--pi-rpc-model openai/gpt-5.5 `
|
| 78 |
--title-boundary-min-similarity 0.25 `
|
| 79 |
--title-boundary-max-similarity 0.85 `
|
| 80 |
+
--threads 4 `
|
| 81 |
--limit 50
|
| 82 |
```
|
| 83 |
|
| 84 |
+
For stratified audits, select heuristic strata explicitly:
|
| 85 |
+
|
| 86 |
+
```powershell
|
| 87 |
+
# High-similarity duplicate/alias keep candidates.
|
| 88 |
+
cargo run --release --manifest-path tools\rust_dmhy_template_apply\Cargo.toml -- `
|
| 89 |
+
--review-title-boundaries-pi-rpc `
|
| 90 |
+
--recipes reports\dmhy_template_recipes.full_top5000.seed.jsonl `
|
| 91 |
+
--title-boundary-decisions-output reports\dmhy_title_boundary_pi_rpc_highsim_keep.jsonl `
|
| 92 |
+
--title-boundary-heuristic-contains keep_similar_duplicate_or_alias `
|
| 93 |
+
--title-boundary-min-similarity 0.85 `
|
| 94 |
+
--title-boundary-max-similarity 1.0 `
|
| 95 |
+
--threads 4 `
|
| 96 |
+
--limit 20
|
| 97 |
+
|
| 98 |
+
# Structural keep candidates normally excluded from ambiguous merge review.
|
| 99 |
+
cargo run --release --manifest-path tools\rust_dmhy_template_apply\Cargo.toml -- `
|
| 100 |
+
--review-title-boundaries-pi-rpc `
|
| 101 |
+
--recipes reports\dmhy_template_recipes.full_top5000.seed.jsonl `
|
| 102 |
+
--title-boundary-decisions-output reports\dmhy_title_boundary_pi_rpc_structural_keep.jsonl `
|
| 103 |
+
--title-boundary-heuristic-contains keep_structural_boundary `
|
| 104 |
+
--title-boundary-min-similarity 0.0 `
|
| 105 |
+
--title-boundary-max-similarity 1.0 `
|
| 106 |
+
--threads 4 `
|
| 107 |
+
--limit 20
|
| 108 |
+
```
|
| 109 |
+
|
| 110 |
Optional controls:
|
| 111 |
|
| 112 |
```powershell
|
tools/rust_dmhy_template_apply/src/main.rs
CHANGED
|
@@ -81,6 +81,8 @@ struct Args {
|
|
| 81 |
lmstudio_timeout_seconds: u64,
|
| 82 |
#[arg(long)]
|
| 83 |
pi_rpc_model: Option<String>,
|
|
|
|
|
|
|
| 84 |
#[arg(long, default_value_t = 50)]
|
| 85 |
audit_max_count: u64,
|
| 86 |
#[arg(long)]
|
|
@@ -1176,6 +1178,23 @@ fn run_rich_annotations(args: &Args) -> Result<()> {
|
|
| 1176 |
Ok(())
|
| 1177 |
}
|
| 1178 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1179 |
fn run_review_title_boundaries(args: &Args) -> Result<()> {
|
| 1180 |
if !(0.0..=1.0).contains(&args.title_boundary_min_similarity)
|
| 1181 |
|| !(0.0..=1.0).contains(&args.title_boundary_max_similarity)
|
|
@@ -1183,34 +1202,38 @@ fn run_review_title_boundaries(args: &Args) -> Result<()> {
|
|
| 1183 |
{
|
| 1184 |
bail!("title boundary similarity thresholds must satisfy 0 <= min <= max <= 1");
|
| 1185 |
}
|
| 1186 |
-
let recipes = load_recipe_values(&args.recipes)?;
|
| 1187 |
-
if recipes.is_empty() {
|
| 1188 |
-
bail!("no recipe rows found in {}", args.recipes.display());
|
| 1189 |
-
}
|
| 1190 |
-
if let Some(parent) = args.title_boundary_decisions_output.parent() {
|
| 1191 |
-
fs::create_dir_all(parent)?;
|
| 1192 |
-
}
|
| 1193 |
-
let mut writer = BufWriter::new(File::create(&args.title_boundary_decisions_output)?);
|
| 1194 |
-
let mut reviewed = 0usize;
|
| 1195 |
-
let mut skipped = 0usize;
|
| 1196 |
if args.review_title_boundaries_lmstudio && args.review_title_boundaries_pi_rpc {
|
| 1197 |
bail!("choose only one title-boundary review backend: LM Studio or Pi RPC");
|
| 1198 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1199 |
let endpoint = format!(
|
| 1200 |
"{}/chat/completions",
|
| 1201 |
args.lmstudio_base_url.trim_end_matches('/')
|
| 1202 |
);
|
| 1203 |
-
let
|
| 1204 |
-
|
| 1205 |
-
|
| 1206 |
-
|
| 1207 |
-
}
|
|
|
|
|
|
|
|
|
|
| 1208 |
|
| 1209 |
-
|
| 1210 |
-
|
| 1211 |
-
|
| 1212 |
-
.and_then(Value::as_str)
|
| 1213 |
-
.unwrap_or("unknown");
|
| 1214 |
let decisions = recipe
|
| 1215 |
.get("title_boundary_decisions")
|
| 1216 |
.and_then(Value::as_array)
|
|
@@ -1224,92 +1247,188 @@ fn run_review_title_boundaries(args: &Args) -> Result<()> {
|
|
| 1224 |
let heuristic_decision = decision
|
| 1225 |
.get("decision")
|
| 1226 |
.and_then(Value::as_str)
|
| 1227 |
-
.unwrap_or("unknown")
|
| 1228 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1229 |
|| heuristic_decision.contains("structural_boundary")
|
| 1230 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1231 |
|| similarity > args.title_boundary_max_similarity
|
| 1232 |
{
|
| 1233 |
skipped += 1;
|
| 1234 |
continue;
|
| 1235 |
}
|
| 1236 |
if let Some(limit) = args.limit {
|
| 1237 |
-
if
|
| 1238 |
-
break;
|
| 1239 |
}
|
| 1240 |
}
|
| 1241 |
-
let
|
| 1242 |
-
|
| 1243 |
-
|
| 1244 |
-
|
| 1245 |
-
|
| 1246 |
-
|
| 1247 |
-
|
| 1248 |
-
|
| 1249 |
-
|
| 1250 |
-
|
| 1251 |
-
|
| 1252 |
-
|
| 1253 |
-
|
| 1254 |
-
|
| 1255 |
-
|
| 1256 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1257 |
)
|
| 1258 |
-
})?;
|
| 1259 |
-
response
|
| 1260 |
-
.get("choices")
|
| 1261 |
-
.and_then(Value::as_array)
|
| 1262 |
-
.and_then(|choices| choices.first())
|
| 1263 |
-
.and_then(|choice| choice.get("message"))
|
| 1264 |
-
.and_then(|message| message.get("content"))
|
| 1265 |
-
.and_then(Value::as_str)
|
| 1266 |
-
.unwrap_or("")
|
| 1267 |
-
.to_string()
|
| 1268 |
-
};
|
| 1269 |
-
let parsed = parse_lmstudio_json_object(&content).unwrap_or_else(|| {
|
| 1270 |
-
json!({
|
| 1271 |
-
"decision": "skip",
|
| 1272 |
-
"kind": "parse_error",
|
| 1273 |
-
"confidence": 0.0,
|
| 1274 |
-
"reason": content,
|
| 1275 |
})
|
| 1276 |
-
|
| 1277 |
-
|
| 1278 |
-
|
| 1279 |
-
|
| 1280 |
-
|
| 1281 |
-
"right_role_index": decision.get("right_role_index").cloned().unwrap_or(Value::Null),
|
| 1282 |
-
"similarity": similarity,
|
| 1283 |
-
"heuristic_decision": heuristic_decision,
|
| 1284 |
-
"model_decision": parsed.get("decision").cloned().unwrap_or(Value::Null),
|
| 1285 |
-
"model_kind": parsed.get("kind").cloned().unwrap_or(Value::Null),
|
| 1286 |
-
"confidence": parsed.get("confidence").cloned().unwrap_or(Value::Null),
|
| 1287 |
-
"reason": parsed.get("reason").cloned().unwrap_or(Value::Null),
|
| 1288 |
-
"source": if args.review_title_boundaries_pi_rpc { "pi-rpc" } else { "lmstudio" },
|
| 1289 |
-
"model": if args.review_title_boundaries_pi_rpc { args.pi_rpc_model.as_deref().unwrap_or("pi-default") } else { args.lmstudio_model.as_str() },
|
| 1290 |
-
"reviewed_at": Utc::now().to_rfc3339(),
|
| 1291 |
-
});
|
| 1292 |
-
serde_json::to_writer(&mut writer, &output)?;
|
| 1293 |
-
writer.write_all(b"\n")?;
|
| 1294 |
-
reviewed += 1;
|
| 1295 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1296 |
}
|
| 1297 |
writer.flush()?;
|
| 1298 |
let summary = json!({
|
| 1299 |
"recipes": args.recipes.to_string_lossy(),
|
| 1300 |
"output": args.title_boundary_decisions_output.to_string_lossy(),
|
| 1301 |
-
"reviewed":
|
| 1302 |
"skipped": skipped,
|
| 1303 |
-
"model":
|
| 1304 |
-
"backend":
|
| 1305 |
"base_url": if args.review_title_boundaries_pi_rpc { "pi --mode rpc" } else { args.lmstudio_base_url.as_str() },
|
| 1306 |
"similarity_range": [args.title_boundary_min_similarity, args.title_boundary_max_similarity],
|
|
|
|
|
|
|
| 1307 |
"generated_at": Utc::now().to_rfc3339(),
|
| 1308 |
});
|
| 1309 |
println!("{}", serde_json::to_string_pretty(&summary)?);
|
| 1310 |
Ok(())
|
| 1311 |
}
|
| 1312 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1313 |
fn load_recipe_values(path: &PathBuf) -> Result<Vec<Value>> {
|
| 1314 |
let file = File::open(path).with_context(|| format!("failed to open {}", path.display()))?;
|
| 1315 |
let reader = BufReader::new(file);
|
|
|
|
| 81 |
lmstudio_timeout_seconds: u64,
|
| 82 |
#[arg(long)]
|
| 83 |
pi_rpc_model: Option<String>,
|
| 84 |
+
#[arg(long)]
|
| 85 |
+
title_boundary_heuristic_contains: Option<String>,
|
| 86 |
#[arg(long, default_value_t = 50)]
|
| 87 |
audit_max_count: u64,
|
| 88 |
#[arg(long)]
|
|
|
|
| 1178 |
Ok(())
|
| 1179 |
}
|
| 1180 |
|
| 1181 |
+
#[derive(Clone, Copy)]
|
| 1182 |
+
enum TitleBoundaryReviewBackend {
|
| 1183 |
+
LmStudio,
|
| 1184 |
+
PiRpc,
|
| 1185 |
+
}
|
| 1186 |
+
|
| 1187 |
+
#[derive(Clone)]
|
| 1188 |
+
struct TitleBoundaryReviewTask {
|
| 1189 |
+
sequence: usize,
|
| 1190 |
+
recipe: Value,
|
| 1191 |
+
decision: Value,
|
| 1192 |
+
similarity: f64,
|
| 1193 |
+
heuristic_decision: String,
|
| 1194 |
+
template_id: String,
|
| 1195 |
+
prompt: String,
|
| 1196 |
+
}
|
| 1197 |
+
|
| 1198 |
fn run_review_title_boundaries(args: &Args) -> Result<()> {
|
| 1199 |
if !(0.0..=1.0).contains(&args.title_boundary_min_similarity)
|
| 1200 |
|| !(0.0..=1.0).contains(&args.title_boundary_max_similarity)
|
|
|
|
| 1202 |
{
|
| 1203 |
bail!("title boundary similarity thresholds must satisfy 0 <= min <= max <= 1");
|
| 1204 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1205 |
if args.review_title_boundaries_lmstudio && args.review_title_boundaries_pi_rpc {
|
| 1206 |
bail!("choose only one title-boundary review backend: LM Studio or Pi RPC");
|
| 1207 |
}
|
| 1208 |
+
let backend = if args.review_title_boundaries_pi_rpc {
|
| 1209 |
+
TitleBoundaryReviewBackend::PiRpc
|
| 1210 |
+
} else {
|
| 1211 |
+
TitleBoundaryReviewBackend::LmStudio
|
| 1212 |
+
};
|
| 1213 |
+
let source = match backend {
|
| 1214 |
+
TitleBoundaryReviewBackend::LmStudio => "lmstudio",
|
| 1215 |
+
TitleBoundaryReviewBackend::PiRpc => "pi-rpc",
|
| 1216 |
+
};
|
| 1217 |
+
let model_name = match backend {
|
| 1218 |
+
TitleBoundaryReviewBackend::LmStudio => args.lmstudio_model.as_str(),
|
| 1219 |
+
TitleBoundaryReviewBackend::PiRpc => args.pi_rpc_model.as_deref().unwrap_or("pi-default"),
|
| 1220 |
+
};
|
| 1221 |
let endpoint = format!(
|
| 1222 |
"{}/chat/completions",
|
| 1223 |
args.lmstudio_base_url.trim_end_matches('/')
|
| 1224 |
);
|
| 1225 |
+
let worker_count = args.threads.unwrap_or(1).max(1);
|
| 1226 |
+
let recipes = load_recipe_values(&args.recipes)?;
|
| 1227 |
+
if recipes.is_empty() {
|
| 1228 |
+
bail!("no recipe rows found in {}", args.recipes.display());
|
| 1229 |
+
}
|
| 1230 |
+
if let Some(parent) = args.title_boundary_decisions_output.parent() {
|
| 1231 |
+
fs::create_dir_all(parent)?;
|
| 1232 |
+
}
|
| 1233 |
|
| 1234 |
+
let mut skipped = 0usize;
|
| 1235 |
+
let mut tasks = Vec::new();
|
| 1236 |
+
'recipes: for recipe in recipes {
|
|
|
|
|
|
|
| 1237 |
let decisions = recipe
|
| 1238 |
.get("title_boundary_decisions")
|
| 1239 |
.and_then(Value::as_array)
|
|
|
|
| 1247 |
let heuristic_decision = decision
|
| 1248 |
.get("decision")
|
| 1249 |
.and_then(Value::as_str)
|
| 1250 |
+
.unwrap_or("unknown")
|
| 1251 |
+
.to_string();
|
| 1252 |
+
if let Some(needle) = args.title_boundary_heuristic_contains.as_deref() {
|
| 1253 |
+
if !heuristic_decision.contains(needle) {
|
| 1254 |
+
skipped += 1;
|
| 1255 |
+
continue;
|
| 1256 |
+
}
|
| 1257 |
+
} else if heuristic_decision.contains("path_boundary")
|
| 1258 |
|| heuristic_decision.contains("structural_boundary")
|
| 1259 |
+
{
|
| 1260 |
+
skipped += 1;
|
| 1261 |
+
continue;
|
| 1262 |
+
}
|
| 1263 |
+
if similarity < args.title_boundary_min_similarity
|
| 1264 |
|| similarity > args.title_boundary_max_similarity
|
| 1265 |
{
|
| 1266 |
skipped += 1;
|
| 1267 |
continue;
|
| 1268 |
}
|
| 1269 |
if let Some(limit) = args.limit {
|
| 1270 |
+
if tasks.len() >= limit {
|
| 1271 |
+
break 'recipes;
|
| 1272 |
}
|
| 1273 |
}
|
| 1274 |
+
let template_id = recipe
|
| 1275 |
+
.get("template_id")
|
| 1276 |
+
.and_then(Value::as_str)
|
| 1277 |
+
.unwrap_or("unknown")
|
| 1278 |
+
.to_string();
|
| 1279 |
+
tasks.push(TitleBoundaryReviewTask {
|
| 1280 |
+
sequence: tasks.len(),
|
| 1281 |
+
prompt: title_boundary_review_prompt(&recipe, &decision),
|
| 1282 |
+
recipe: recipe.clone(),
|
| 1283 |
+
decision,
|
| 1284 |
+
similarity,
|
| 1285 |
+
heuristic_decision,
|
| 1286 |
+
template_id,
|
| 1287 |
+
});
|
| 1288 |
+
}
|
| 1289 |
+
}
|
| 1290 |
+
|
| 1291 |
+
let mut outputs = if worker_count == 1 || tasks.len() <= 1 {
|
| 1292 |
+
review_title_boundary_chunk(
|
| 1293 |
+
&tasks,
|
| 1294 |
+
backend,
|
| 1295 |
+
&endpoint,
|
| 1296 |
+
model_name,
|
| 1297 |
+
args.pi_rpc_model.as_deref(),
|
| 1298 |
+
args.lmstudio_temperature,
|
| 1299 |
+
args.lmstudio_timeout_seconds,
|
| 1300 |
+
source,
|
| 1301 |
+
)?
|
| 1302 |
+
} else {
|
| 1303 |
+
let chunk_size = ((tasks.len() + worker_count - 1) / worker_count).max(1);
|
| 1304 |
+
let pool = rayon::ThreadPoolBuilder::new()
|
| 1305 |
+
.num_threads(worker_count)
|
| 1306 |
+
.build()
|
| 1307 |
+
.context("failed to configure title-boundary review thread pool")?;
|
| 1308 |
+
let chunk_results: Vec<Result<Vec<(usize, Value)>>> = pool.install(|| {
|
| 1309 |
+
tasks
|
| 1310 |
+
.par_chunks(chunk_size)
|
| 1311 |
+
.map(|chunk| {
|
| 1312 |
+
review_title_boundary_chunk(
|
| 1313 |
+
chunk,
|
| 1314 |
+
backend,
|
| 1315 |
+
&endpoint,
|
| 1316 |
+
model_name,
|
| 1317 |
+
args.pi_rpc_model.as_deref(),
|
| 1318 |
+
args.lmstudio_temperature,
|
| 1319 |
+
args.lmstudio_timeout_seconds,
|
| 1320 |
+
source,
|
| 1321 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1322 |
})
|
| 1323 |
+
.collect()
|
| 1324 |
+
});
|
| 1325 |
+
let mut combined = Vec::new();
|
| 1326 |
+
for result in chunk_results {
|
| 1327 |
+
combined.extend(result?);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1328 |
}
|
| 1329 |
+
combined
|
| 1330 |
+
};
|
| 1331 |
+
outputs.sort_by_key(|(sequence, _)| *sequence);
|
| 1332 |
+
|
| 1333 |
+
let mut writer = BufWriter::new(File::create(&args.title_boundary_decisions_output)?);
|
| 1334 |
+
for (_, output) in &outputs {
|
| 1335 |
+
serde_json::to_writer(&mut writer, output)?;
|
| 1336 |
+
writer.write_all(b"\n")?;
|
| 1337 |
}
|
| 1338 |
writer.flush()?;
|
| 1339 |
let summary = json!({
|
| 1340 |
"recipes": args.recipes.to_string_lossy(),
|
| 1341 |
"output": args.title_boundary_decisions_output.to_string_lossy(),
|
| 1342 |
+
"reviewed": outputs.len(),
|
| 1343 |
"skipped": skipped,
|
| 1344 |
+
"model": model_name,
|
| 1345 |
+
"backend": source,
|
| 1346 |
"base_url": if args.review_title_boundaries_pi_rpc { "pi --mode rpc" } else { args.lmstudio_base_url.as_str() },
|
| 1347 |
"similarity_range": [args.title_boundary_min_similarity, args.title_boundary_max_similarity],
|
| 1348 |
+
"heuristic_contains": args.title_boundary_heuristic_contains.as_deref(),
|
| 1349 |
+
"threads": worker_count,
|
| 1350 |
"generated_at": Utc::now().to_rfc3339(),
|
| 1351 |
});
|
| 1352 |
println!("{}", serde_json::to_string_pretty(&summary)?);
|
| 1353 |
Ok(())
|
| 1354 |
}
|
| 1355 |
|
| 1356 |
+
fn review_title_boundary_chunk(
|
| 1357 |
+
tasks: &[TitleBoundaryReviewTask],
|
| 1358 |
+
backend: TitleBoundaryReviewBackend,
|
| 1359 |
+
endpoint: &str,
|
| 1360 |
+
model_name: &str,
|
| 1361 |
+
pi_rpc_model: Option<&str>,
|
| 1362 |
+
lmstudio_temperature: f64,
|
| 1363 |
+
lmstudio_timeout_seconds: u64,
|
| 1364 |
+
source: &str,
|
| 1365 |
+
) -> Result<Vec<(usize, Value)>> {
|
| 1366 |
+
let mut pi_rpc = if matches!(backend, TitleBoundaryReviewBackend::PiRpc) {
|
| 1367 |
+
Some(PiRpcClient::start(pi_rpc_model)?)
|
| 1368 |
+
} else {
|
| 1369 |
+
None
|
| 1370 |
+
};
|
| 1371 |
+
let mut outputs = Vec::with_capacity(tasks.len());
|
| 1372 |
+
for task in tasks {
|
| 1373 |
+
let content = if let Some(client) = pi_rpc.as_mut() {
|
| 1374 |
+
client.review(&task.prompt).with_context(|| {
|
| 1375 |
+
format!(
|
| 1376 |
+
"Pi RPC title-boundary review failed for template {}",
|
| 1377 |
+
task.template_id
|
| 1378 |
+
)
|
| 1379 |
+
})?
|
| 1380 |
+
} else {
|
| 1381 |
+
let response = call_lmstudio_title_boundary(
|
| 1382 |
+
endpoint,
|
| 1383 |
+
model_name,
|
| 1384 |
+
lmstudio_temperature,
|
| 1385 |
+
lmstudio_timeout_seconds,
|
| 1386 |
+
&task.prompt,
|
| 1387 |
+
)
|
| 1388 |
+
.with_context(|| {
|
| 1389 |
+
format!(
|
| 1390 |
+
"LM Studio title-boundary review failed for template {} via {endpoint}",
|
| 1391 |
+
task.template_id
|
| 1392 |
+
)
|
| 1393 |
+
})?;
|
| 1394 |
+
response
|
| 1395 |
+
.get("choices")
|
| 1396 |
+
.and_then(Value::as_array)
|
| 1397 |
+
.and_then(|choices| choices.first())
|
| 1398 |
+
.and_then(|choice| choice.get("message"))
|
| 1399 |
+
.and_then(|message| message.get("content"))
|
| 1400 |
+
.and_then(Value::as_str)
|
| 1401 |
+
.unwrap_or("")
|
| 1402 |
+
.to_string()
|
| 1403 |
+
};
|
| 1404 |
+
let parsed = parse_lmstudio_json_object(&content).unwrap_or_else(|| {
|
| 1405 |
+
json!({
|
| 1406 |
+
"decision": "skip",
|
| 1407 |
+
"kind": "parse_error",
|
| 1408 |
+
"confidence": 0.0,
|
| 1409 |
+
"reason": content,
|
| 1410 |
+
})
|
| 1411 |
+
});
|
| 1412 |
+
let output = json!({
|
| 1413 |
+
"template_id": task.recipe.get("template_id").cloned().unwrap_or(Value::Null),
|
| 1414 |
+
"template": task.recipe.get("template").cloned().unwrap_or(Value::Null),
|
| 1415 |
+
"left_role_index": task.decision.get("left_role_index").cloned().unwrap_or(Value::Null),
|
| 1416 |
+
"right_role_index": task.decision.get("right_role_index").cloned().unwrap_or(Value::Null),
|
| 1417 |
+
"similarity": task.similarity,
|
| 1418 |
+
"heuristic_decision": task.heuristic_decision,
|
| 1419 |
+
"model_decision": parsed.get("decision").cloned().unwrap_or(Value::Null),
|
| 1420 |
+
"model_kind": parsed.get("kind").cloned().unwrap_or(Value::Null),
|
| 1421 |
+
"confidence": parsed.get("confidence").cloned().unwrap_or(Value::Null),
|
| 1422 |
+
"reason": parsed.get("reason").cloned().unwrap_or(Value::Null),
|
| 1423 |
+
"source": source,
|
| 1424 |
+
"model": model_name,
|
| 1425 |
+
"reviewed_at": Utc::now().to_rfc3339(),
|
| 1426 |
+
});
|
| 1427 |
+
outputs.push((task.sequence, output));
|
| 1428 |
+
}
|
| 1429 |
+
Ok(outputs)
|
| 1430 |
+
}
|
| 1431 |
+
|
| 1432 |
fn load_recipe_values(path: &PathBuf) -> Result<Vec<Value>> {
|
| 1433 |
let file = File::open(path).with_context(|| format!("failed to open {}", path.display()))?;
|
| 1434 |
let reader = BufReader::new(file);
|