test(pf-bitstream): replay real host captures through the planners + HEVC goldens

M0's capture hook has been in since 119ec0dd with nothing consuming its
output. corpus_replay.rs is that consumer: point PF_CORPUS at an
au-<stamp>.<codec> capture and every AU walks back through the H.264 or
H.265 planner, asserting no errors and no warnings — a clean capture of
a healthy session must plan whole. Ignored by default (captures are
hundreds of MB and live outside the repo).

It earns its keep immediately. Captured on .173 against the live host
(NVENC, 2800x1260, ~30 s each, client-side codec pin only — no host
config touched):

  h265  1133/1133 AUs planned, 0 errors, 0 warnings
  h264  1514/1514 AUs planned, 0 errors, 0 warnings

The HEVC number is the point: it is the FIRST validation of the WP-1
h265 planner against real host output rather than the vendored
conformance vectors, and it lands before the client's HEVC rung exists
to produce on-glass evidence.

Two real-capture facts the harness had to learn, both from this run:
ending a capture means killing the client, so the final .idx line is
routinely half-written and the final AU's bytes may not all have landed.
Both are tolerated at the TAIL only — a malformed line anywhere else, or
a gap the data cannot cover mid-file, still fails loudly rather than
silently replaying a subset.

Also adds tests/data/test-25fps-h265.nv12.sha256: 250 per-frame NV12
hashes of the vendored HEVC vector from libavcodec's software decoder,
cross-checked frame-for-frame between two independent FFmpeg builds
(8.0.1 in pf-lxcheck2, 8.1.1 from Homebrew) — the sibling of the H.264
goldens, ready for WP-2's parity leg.

Gates: fmt clean; pf-bitstream clippy clean, 69 tests green (the replay
stays ignored in normal runs).
This commit is contained in:
2026-08-06 01:08:50 +02:00
parent 55bc664eca
commit c985438db1
2 changed files with 500 additions and 0 deletions
+232
View File
@@ -0,0 +1,232 @@
//! Corpus replay: walk a captured real-host stream through the planners.
//!
//! The M0 capture hook (`PUNKTFUNK_DUMP_VIDEO=<dir>` on any desktop client) writes
//! the exact decoder input of a live session — `au-<stamp>.<codec>` plus an `.idx`
//! sidecar carrying `offset len flags complete` per AU. This harness feeds those AUs
//! back through [`pf_bitstream::h264::H264Planner`] / [`pf_bitstream::h265::H265Planner`]
//! and asserts the planner survives a REAL host stream: every AU plans (bar the
//! deliberate skips), no panic, and the warnings are only the ones a clean capture may
//! legitimately produce.
//!
//! Why this exists separately from the vendored conformance vectors: those prove we
//! match the spec's own test streams, and the on-glass sessions prove the whole pipe —
//! but between the two sits "does the planner handle what OUR five host encoder
//! families actually emit", which is the question the corpus was captured to answer.
//! For HEVC this is the ONLY pre-wiring validation against real host output (the
//! client's HEVC rung is still being built), so it runs long before M3 finishes.
//!
//! Ignored by default: captures are hundreds of megabytes and live outside the repo.
//! Run one explicitly —
//!
//! ```text
//! PF_CORPUS=/path/to/au-1785970273.h265 \
//! cargo test -p pf-bitstream --test corpus_replay -- --ignored --nocapture
//! ```
//!
//! The `.idx` sidecar is found next to the data file (`<data>.idx`); the codec comes
//! from the extension, matching the capture hook's own naming convention.
use std::path::Path;
use std::path::PathBuf;
/// One captured access unit: its byte range in the data file, plus the wire bits the
/// byte stream itself cannot carry.
struct CapturedAu {
offset: usize,
len: usize,
/// The wire `flags` byte (`USER_FLAG_*`) — kept for the RFI/intra-refresh legs,
/// which discriminate on it.
_flags: u32,
complete: bool,
}
/// Parse the `.idx` sidecar: one `offset len flags complete` line per AU, `#` comments
/// and blank lines skipped (the hook writes none today, but a hand-trimmed corpus file
/// is a thing a human will produce).
///
/// A malformed FINAL line is dropped with a note instead of failing: ending a capture
/// means killing the client, so the last buffered line is routinely half-written (the
/// hook's own docs call a truncated last AU acceptable). Anywhere else a malformed line
/// means the sidecar is corrupt and the run must not quietly replay a subset.
fn read_index(path: &Path) -> Vec<CapturedAu> {
let text = std::fs::read_to_string(path)
.unwrap_or_else(|e| panic!("cannot read the index sidecar {}: {e}", path.display()));
let lines: Vec<&str> = text
.lines()
.filter(|line| !line.trim().is_empty() && !line.trim_start().starts_with('#'))
.collect();
let last = lines.len().saturating_sub(1);
let mut out = Vec::with_capacity(lines.len());
for (n, line) in lines.iter().enumerate() {
match parse_index_line(line) {
Some(au) => out.push(au),
None if n == last => {
println!("note: dropping a truncated final index line ({line:?})");
}
None => panic!("index line {n} is malformed: {line:?}"),
}
}
out
}
/// One `offset len flags complete` line, or `None` when it is not four parsable fields.
fn parse_index_line(line: &str) -> Option<CapturedAu> {
let mut it = line.split_whitespace();
let num = |raw: &str| -> Option<u64> {
match raw.strip_prefix("0x") {
Some(hex) => u64::from_str_radix(hex, 16).ok(),
None => raw.parse().ok(),
}
};
let offset = num(it.next()?)?;
let len = num(it.next()?)?;
let flags = num(it.next()?)?;
let complete = num(it.next()?)?;
Some(CapturedAu {
offset: offset as usize,
len: len as usize,
_flags: flags as u32,
complete: complete != 0,
})
}
/// The capture named by `PF_CORPUS`, or `None` when the variable is unset.
fn corpus_from_env() -> Option<(PathBuf, Vec<u8>, Vec<CapturedAu>)> {
let path = PathBuf::from(std::env::var_os("PF_CORPUS")?);
let data = std::fs::read(&path)
.unwrap_or_else(|e| panic!("cannot read the capture {}: {e}", path.display()));
let mut idx = path.clone().into_os_string();
idx.push(".idx");
let mut index = read_index(Path::new(&idx));
// Same truncation story on the data side: the final AU's bytes may not all have
// reached the file before the client died. Drop AUs the data cannot cover — but
// only from the tail, so a short file can never silently hide a middle gap.
let covered = index
.iter()
.take_while(|au| au.offset.saturating_add(au.len) <= data.len())
.count();
if covered < index.len() {
println!(
"note: dropping {} index entr{} past the end of the data file (truncated capture)",
index.len() - covered,
if index.len() - covered == 1 {
"y"
} else {
"ies"
},
);
index.truncate(covered);
}
assert!(!index.is_empty(), "the capture's index is empty");
Some((path, data, index))
}
/// Per-AU outcome tally — what the run reports and asserts on.
#[derive(Default)]
struct Tally {
planned: usize,
skipped: usize,
errors: Vec<String>,
warnings: Vec<String>,
partial: usize,
}
impl Tally {
/// A clean capture of a healthy session must plan every complete AU. Errors are
/// hard failures; warnings are printed and capped — `MissingReference` on a stream
/// that never lost a packet would mean the planner invented a gap.
fn assert_clean(&self, total: usize) {
println!(
"planned {} / skipped {} / partial-AUs-ignored {} / errors {} / warnings {} \
(of {total} captured AUs)",
self.planned,
self.skipped,
self.partial,
self.errors.len(),
self.warnings.len(),
);
for w in self.warnings.iter().take(20) {
println!(" warning: {w}");
}
for e in self.errors.iter().take(20) {
println!(" ERROR: {e}");
}
assert!(
self.errors.is_empty(),
"{} AUs failed to plan — first: {}",
self.errors.len(),
self.errors[0],
);
assert!(
self.warnings.is_empty(),
"{} planner warnings on a clean capture — first: {}",
self.warnings.len(),
self.warnings[0],
);
assert!(self.planned > 0, "no AU planned at all");
}
}
#[test]
#[ignore = "needs a capture: PF_CORPUS=<au-file> (see the module docs)"]
fn a_captured_host_stream_replays_through_the_planner() {
let Some((path, data, index)) = corpus_from_env() else {
panic!("PF_CORPUS is unset — see the module docs for the invocation");
};
let ext = path
.extension()
.and_then(|e| e.to_str())
.unwrap_or_default()
.to_owned();
println!(
"replaying {} ({} bytes, {} AUs, codec {ext})",
path.display(),
data.len(),
index.len(),
);
let mut tally = Tally::default();
// The planners take one COMPLETE AU. A partial AU (the wire's shard split) is the
// pump's business, not the planner's — count and skip rather than feed a fragment.
let complete: Vec<&CapturedAu> = index.iter().filter(|au| au.complete).collect();
tally.partial = index.len() - complete.len();
match ext.as_str() {
"h265" => {
let mut planner = pf_bitstream::h265::H265Planner::new();
for (i, au) in complete.iter().enumerate() {
let bytes = &data[au.offset..au.offset + au.len];
match planner.plan_au(bytes) {
Ok(plan) => {
tally.planned += 1;
for w in &plan.warnings {
tally.warnings.push(format!("AU {i}: {w:?}"));
}
}
// The spec's own skip (8.1.3): decode nothing, show nothing, the
// stream is healthy — never an error (the WP-2 contract note).
Err(pf_bitstream::h265::PlanError::RaslSkipped { .. }) => tally.skipped += 1,
Err(e) => tally.errors.push(format!("AU {i}: {e}")),
}
}
}
"h264" => {
let mut planner = pf_bitstream::h264::H264Planner::new();
for (i, au) in complete.iter().enumerate() {
let bytes = &data[au.offset..au.offset + au.len];
match planner.plan_au(bytes) {
Ok(plan) => {
tally.planned += 1;
for w in &plan.warnings {
tally.warnings.push(format!("AU {i}: {w:?}"));
}
}
Err(e) => tally.errors.push(format!("AU {i}: {e}")),
}
}
}
other => panic!("no planner for a .{other} capture (h264/h265 only today)"),
}
tally.assert_clean(index.len());
}
@@ -0,0 +1,268 @@
# SHA-256 per decoded frame of test-25fps.h265, DISPLAY order — 250 frames.
# Each frame is the 320x240 picture as tightly packed NV12:
# Y plane 320*240 bytes, then interleaved UV 320*120 bytes = 115200 bytes/frame.
# (This vector carries no conformance window — coded size IS display size.)
#
# Generated from libavcodec's software decoder (H.265 decoding is exactly
# specified — every conformant decoder is bit-identical), 2026-08-06, and
# CROSS-CHECKED between two independent FFmpeg builds that agreed on all 250
# frames: 8.0.1-3ubuntu2 inside the pf-lxcheck2 image (docker run --rm
# --platform linux/amd64, ffmpeg via `apt-get install -y ffmpeg`) and 8.1.1
# from Homebrew on macOS/arm64:
#
# ffmpeg -i crates/pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/test-25fps.h265 \
# -f rawvideo -pix_fmt nv12 -fps_mode passthrough ref.yuv
# # then split ref.yuv into 115200-byte frames and sha256 each
#
# Sibling of data/test-25fps.nv12.sha256 (the H.264 goldens) and consumed the
# same way by the HEVC parity test.
ef4900effa72cbc00cff38938bc558cc5d70b31ca268aaf3c01c4268a9d8066c
fdbae4473d24c3adcef59a51c1449304c2fa271412622efceeba5ee7437b5529
9aab5493a8fe3dfbd3f1eab89bf67a4964998bf8f1229c672ddf2d0d5fe26edb
52797a59351b0a02069a3d8b2022ad7e06be4e03a7ab9483e09c0b03e4dbfec9
9c68407853977ddd95dda04b77676e983fd7ad4665bbbe65497ea5e91f5b19d9
1d7e6adc6a225e1bcd2c5cc598c0342472edd2d7dcc162c38872fc665b57f3d5
4633e66a0ec77f26ab955bea209b1e976947ae87d8405e4a70be2d8e6ab69625
6cce1546ee5c1d414353a990aeb6cd3035199196cc6bd527d1f7985247c30cfc
6e5eb5c2286156d4c7406c9bba159348618799fdf2d46d470ad4f0fe2c6671dc
25b0af55a492eaa503349ab45f1d6449df832361ac60d2910eade5380c5b0789
dcae9606d93f9405c95e5a03699beeae1442240303df2d457bb3b5fd5e2cbd18
d7bcf9616f062b9cbdaa076db0aa8fe0e60ef83b51c1336276183fa4d6f2f0cb
003e43b5d1e731ae234146763c1f65b4c4e2a67fdab3d938b7ba442be360a520
3bbf1a62250deeae73fdf77ff34c2c850886557bc79815a2c968a5d8968a67ba
d8fff092b581e7807b77df684e74f97013b6c69d4610ad17f08855585c3ef5d6
52d3d01f8f147d8574295859ba1f7437ff11d57eafb5c5325b55c7533abf922e
e75eef989830cbd6c8dc3957fcd96e1061f861c5f12eaefadd21f600ce0998a0
9ffd2496189a6384eddda2e752a719e27c6c69e5fc74330b75fa7e4c928cfd55
ff304806867abf9d6d683c0d3bce95473cbad3252e815463e9eb5a1b5fabfd6d
9320a5e1fd33ec1c8b3d2964af8050bbcec219b3287b83c2cbe6f61e8d4f3cd7
9816da3390b510218fc8605398f9cee8d0c8561e2933f2e2a1f771023a7116ea
198b078fda1f6f1402a53e8c73b99544abb8c4091efab68e7221af73732fc10c
fb13ab23a5e012e4ff0cdbba1af7261a636940831c1cbbe1065fdd4eeb4754f4
73fca4a423291a3ff6c787a7660085732a687d7e061dff235a29d83ceb96e95b
195ce227acdcfa1a4cd4fe35eff3291ebdd5f3fe7545509e32e17c744eb98b4a
706aee7736f9c599b48dcd156cb27faaaae51f3fe85b14dc12402207033aed17
e061a0ab22441f9b2be0fd111ca67248ea40bb3f5d3b280b3c52648eed8444b1
93be6f5a81550ecca250ee0c145e5c4135d05ef0f4a78095f94e85070cae9db9
465723432c68de41e96fc32e562812343a9fdecd53b9699fcc6770b5f9e4b002
874b39b128db5b47160cf0d8bcc0040063e23dc5b7f0172a8149f78e7ab57195
3cee067706084dd8740ba200e3eb1955c93e1df4aaecc298453560e61f658271
7ea42e3f7c7a6488057d4cb7db1f7dd61f505cdcf8e71bcaa33eb00b734018bf
55b00704e979ac7bf2e4961dfe12f1ef5c559a421e24e3243f943a013e679fe0
e91f2096b0bde6bef5762f2765d2ce30c77f9e1a9adc151bd07277fb715c8131
08d1391f8876e67cd086d79a703401da70abdd37e3522445830e40cd820cf3ba
87531fd1641407d8eee5888068c9655920976deaaace01b0c6b6766dace53d62
1993c8add817ed50882c865eff2c82f93f1c8f050e1ee3171d9518e3a6ac0563
f682098c0cfa38af29ecb40572aae48c4663e11c5ba9ae256871a1fc62111dd7
dca4d5cad38a35d15ecd4ef02e2a2effc6c103ac4f27ed61518416bfb8084f72
33a6d608d4dcb2cb47d648ff4637b62e65b4a769093ba0bbf39ef151e174cf5f
814d05ff585e21676fe08a867ca4593c65870b81d5bb7ae51d8fb795371bf9e5
91339266713bb79af8d547ad888a010b7ad56debc938e4ecd9f3542a8d148e40
945258f865bb6b795d06609d57ac77ee5d863bf19fb35785ac7578d46e229cb8
2b2619f2814b08675b263e38ae8a50d816fca71d6f014f6058cea2b4dae74aee
89b8983416633c225519b3621bf65601a0f6c0f46de5ca3250a9b0c34edff73f
0a007d2eacaf06f629ef854450fca7d83d6f6e1bab60a1145466dc157082f877
e8550a8a431f623ccfc8457c4f171a6c9bfdc1c67807fe1fb45cf4bdbc14f7d1
aafa9b366c4d560b9994366b1ec3a2f71c861191316b84e6b46ba7ddf79c19ee
4ec7b250bb49ec17b34a090f36f8cd07692829dcba1ee939a4526e89366ba1a5
c00e70ec1b43cea1b3d2909d61cd53239f428085b6769913307c9006bfeae83c
b8744028170518a8542d4f37ff05990230ea1ab87d71896b0fb41866b3859e57
cc9b833a5725bae1c6e9d198c148ef45fced9b34a36fcf043cfd2774baf9ac55
81f3666280e765cc8641fcfb0a65fac70175e18c7ab701534a5499ffe3fa8543
f92faf17adfb611afa432c2c4f3c192ca24246a9613601ca109bcec0d8a18c56
6acda837d6691b80b11c9c83fb41108b0aaefd1a90b5bdae28429542ae998f5c
94db3458a1fac5e9cd5a9ec07de06c659a20051b80bf71b2435d174604e502bd
cdbb03c8d67aeef96c7073346a87b1a41e866c2fec75a5003ab43d10ba953457
2f05ec023ce09ce00b3fdd277e0da4aabe1e1a073e9612632fb5f5c95103caa7
5587ef3923591036daf4d471a8a131d04fff2a6612e2e7788ac4662a8a3e16eb
19c7c1534c40e8a4dd7a6461fcd5d8695951dca24d7d2923b5ca12cb4160afe1
95bb6aefb8ac74e52e3ef64e224a64eecb08d82ff3449d1cc1b8b44888a2ca38
1278e669f16f311582ea9456e7361c57d4b4748c9902c780c4d9d62c7e603f1c
0d77145396334c2f3008b72441d594f7c3e110da95aef11d6bede46f49c79be9
099ea10831ceda528df779d1c7b679cdc1d251f10cfc39f4950f7fb3a3cebd36
8cb5a50ee01d37f8501884be3cd5239f5e57892901afedc4ddc3a9fecf435b0a
5c4f777977b786ab5e80f6bd7ea9880aae7fd8d8cf9e4d79016020d4fc916c22
d635fab89355529ee6661f8e867e5361bd28773422f8478939dd806162292346
baebab9b93c6dbb63ea19468538d9663a2f9fef5bae21dba46161454ca05c770
22f4c44eec9c70d32b05cf0eccfbdf5b477b0f93bc61a17f9b5b4c5a7dbc2ebf
fecadfb3ec25bd00929059e9849da387ca0cee8eec9f0792128f3256eddd5bd7
48065431ce0b3f8f0ad5f77fc30fa29eb961b1ba5e6a97c0bf41c5d3c0ea8518
b575ccc73436b7937f98ec1847fd54d7c7a3a086d08adcff9d226613efd5bac6
be5d094c2a2ab5feb6c988c68e9f2d6eee73bc69e8c51ed24786325d185ad5f6
6c16634620280926af758f013b66c2a75a79ea67a90184965ea1bf91b9f51696
0b63b9eedc87d047474f9c46e71dee75ca1e3a5db5cb0c9d7ea0627fa8c82876
02052e7622fa96d6a0696f5c27d632ee49caedc5ac908a5c9f6a8d6d475be6e3
ceeff7e73cca36a48a775f2e6b773d7c968c20de18bef4460dca582e6ba4b894
693663aaf34e534a4f6d2ae7846aa26aa1d73b4bd9f854dd70bdb6a41b0cbb65
474a10f6eefba5c7d16efd99fee59e0b762380b0f3acbb91e76bdc18a6dc9c02
4c005f6dbb86ba5ef0001a551af8b4e4aa996e5f27c23658ea3002d319e3c53a
9968c5c0966a246dad5e0b49668d3d1caccc12a7722aaff677a6ad63c32b961e
a1d57c958b92e6a8d557893c62cea08d5959dd34a1421bf1fba02adf7acf0244
f885bb6f13840be75c88223d07105f0ccbd4aa678ac2e76ca347739b0e3158b0
644526f73200746ed78f8d0a35bacf486b17c6bb890f58e12c433ccdad59a37f
2beef1de03e18970f81792b0d247fdc9112d603dca852507b48d3585c86a7fa6
b76fe733ccb80eb233ccd263dbfe264b6c298a93a3847159ab19fd8d9566d63d
cc7cfa0dfd02fec28ddc9504cf0e4a2656c2b3e7e2f13207b9979f9930408ea6
fa6546a8828e3e3c75b1a60c0681db322d521fd4dd3fc9b005ccfb42d58fd9ba
d1b2527753951c11ff69e4db8323c058e40f411bfa4e23915f5675e30cd43b77
5eaf026663142af2ba32628a4589aaa143977ecba28181730172099c6f749abb
9d18abd96a6548cb643b39a2499b1f718713dbd92f66f35392fa0f9fcdf4a81b
38ce5c4261c740ab054dac484af6afe58bf5eacd95fd322113cf532f362e0ea2
55852ee3a2f92db0bdfc64c2693ff5a43ed7348a6ea947aaf9646b964c1842e7
ba7488d71bf856cebde72d96245a582efae3d3392dc75615fdce2146b123cb0f
3e4c55e47febd86861cf4a7b56cb2e6fdcb1d9542d521ab04f57643d844d2f81
8df056cf2f0d1e13c1d1a419251b09a9e1edd164e5c9ace7525be2c08d7cb84f
2e7fc74a622da56c8ad17d66f7fa2bd5a466dfe4147db80b7a58dbc93158a0ea
ffac7a103ab051aad9ff90fa43f9d98c149aaa6057d59660a70256bc9ab3951a
3725bd25b394076327a8f6ff010fb9e4b7662d67b48c80693997d434eb541399
fd807e07cd0f003ac3d834487bd7526b000d3ffd79295ba92fc10caf3e5bd6ad
071469b129dc9ef55e67f773ad5c2ef6498b728169e98910dc5923b4181e3ad2
512ced1859982b5bbb58f48a5ae8006c56bd28e165f0a761c38bf214d2636983
8a663c0abe78913974d29f4380b7addc46612d55fb431c11042bb7985f22d501
92ea5506c94d885aedb9dfa7f884a43ed8289b49542b67861e576cfea4d62b11
a29dde155a03511735f9370ce23b95bfd98868434ddf6e9a3669c2edd0ad1d8f
1688fd2fe7b5f7e4cfcc40d3afff903bd0a5cb8536c0d7da4d53b90544b03227
96f593ab7339f0cb02bec07b6f5bcc11d2494ced35a90d4037c961a2e284d914
9450d79524043cc3f2afe0dda19b6a5a06de7128ad9826802b798255f72aaf00
4dee54c01448ed0595fe1c0108c97031c48de96ea425c8f8c2e5b3382e862a3e
24bb1d429aee55c9d2c719777d15a3e8116fd2074a569a5e44a8d5de6d11fe8f
fe0255625438d6403fd663f60eab05ff990780054eb530dbfaa3552a978cfdc5
452961eb29a947dd2818409c8d5e283ebf47281eebeb9c368164b22dadb315a5
e49ad550b30140b2b99b6edd0e1a7873f3ba0145829037c2490467ddfcfc439a
f02dd0ac342330223e9c0ae56ed6a9efc3a0c69d8bb4c036943dd16f654f10bf
60352cf761e2cd8ba2cc0025c45e715a8719fabe78d5de7a382da4b648704ea7
3b79506fc18e01a1168f880712c539f0a520712dd62d2f824b6b754ce034f1f2
3c16234ed4606128fca31196a81f769ac69a4261aa176bf8c8d86b8699177926
6a98fcfcef5d1ed813ba3f2f66f5b1bd11729430187a7366dcbe9617f2914646
5683c679c3c0fb24b740ef4582c787591f10dcffc9e46037422829f5b58a1501
9b4c0ca82d8440b630a569a8d6525384beb26ca7d54b7a1ddb1b83a00e134701
5693915c011d2ad738b84c8a04cfb8081006b8835ec655e76d829e5dbcbe0688
1ddbeb8cbe7d0d8ce61a94e3ca35730a07e59bdbb692cc82dfa3ea0ff9ba5979
ca10afdd882627e9a6cb5d389c71fa015cb0015124c7445295ac42660a01c491
7aa406047f3fdc2a07d6d228b30535734689c99d93e8c5c7865044187ca0d3b8
8e3f8d26612b58f985ed778f1ad5f6cca0be49acee5b7d4d09ab2b886a3327bd
9edb22ab753a8cb66392d301144df20c7e67e3afe6bc9d1c3143087ecf26120f
fc4d040db9172642d710b4edabca7e5444fdc9099f9867b5942e200da1d6373a
1cfd8b95c000cc30892061db6dac6323f2f7a654c30d0ed024368997ac65e673
f9ebda67091c2b26e7205bdab431299d982dace99cedfec13c80cfd3195286c0
5cea0e5febf079803e85b57bf45d3d563d0e8fa1e45844af18609077497fbd3c
2c4c74504660b5222f2135407805000fe1a41df48c47b28685e57020c6d21061
d49cffec62c461a04292556bb21f78861556b058db006143696e53cee077d26b
9d7ad96746eef142fec236232c0d275943a5258a40fe3fa384aaa5614856cbc2
958cde369e11df3a7bc534ac549fc11b4a6e325fc02942f49c68c47e99ed65bc
a05043d7198834dd8da3d544dd7c11d3f03a3389a422fd0a0be732824b587047
700a4641d8ee0a7ab2b0a09421755ee6223ef1f7b6436a96eaa479140361b40b
16d487df87dc05fa10373f139558597f0c82b9a22828de0ec462fae72ac09124
73a8de0a8f0652405aad48cb13765767c80286ef190dfc1a10234fae0ee0dc71
19f30498e93062ddf30e284bb44b9c6b6ee5c2ef6e143d40cf7dd76b669bf670
93de878f03e69fce3ec00963df3bb941713bf225b665b0baede46e4a9559b567
fc55a32f7fc41925a5bdf7fcf34513b90b6acc326771fd77db7fc0f4572db395
e84bffc862d0d017903ec43414b0659b9720d0a4d646be3d1576a168a4333ab3
ef655d0f2ee29f23409d1a8689710e024aae636c1ecfe8bc1915774a94732f8d
95c63e9092ba3a5887e9ba2044aea96315b25e55af2fbadbad50345fb98629fa
15501a54f24943a19a663263c5459d31c45441147cd58eba4935ab67d1bed291
bedcf37b2101e06e9f76f85df28e70574bff094ddee96fb3b4733da63551e22a
e06487f3691cb56df61d13e838d62a403e7cf4cbf399c89e98a7d04597e91653
483596c621e2646ffe668743eb0e55269bc7dde6188ffb80c086b215d73a9717
a84fcc692a5ae998f082fb21e429076d59c48fa6880309bbe692e6f0b5c0ab86
4aace8b7ea4891aa80f8df369689e0dd587d88d55160d40ff1fc03dd4fb7fe44
b0b13ef2e6684489fe0af1ddcc2dc2e04e833d372b7c4838d1cd173da993265d
6ca96a7be457ba2bf06aba287fdad92bf356d31723d392e654ff352d480b5da0
69554f33448ca613f466adbb14748ce2001518c861397100579340b9d9c71dee
baebb6de906b34a88a890274e491ffe00ce28b003b0ef1331c484740c036a30d
841a7b7efd05b4f8aadf6bb0074067a64b118c20505f317babcdebcdac05b9aa
d2eeff202d639a4c0ff451caad9cc7a7f4bb8aa62e6659671fadc4ba85a7eb29
4ebc9035f1db8feb6e56f47a180b4108ee374c7442ea5cb2ba4fa59427d88fc6
b7e2988ff896ed2618a17766dff44839b23b934108a1d75948ed0826c8129d42
9eb4b2490eabc51f7fcc75c5aad419208238ed6807ff109ec02cf0f6ac1e12dc
4ba2d5fc6b9b27c53b4217b04f70dc219073a9c1cb0863e040d9973c88899084
e26701bcba0f2014ca93eee23893e020bf681e071c9e62d0fb75619de7de715c
ec829d9898ba7eccb51af41e5412255bfe93e61137f56a21d4999911fc154bfe
ad47b88c0fc7750dd1a5cc27cf134638a9270342e957aef81e517a5a519933fd
1d39f1ebd301526344a29c8d07049a389ce157e8a0184fb34e38ad3c2a851afd
20d4699ce3a43154a62ef5cbf8f14bc47cfc344eff92ec78e4e6108d2827ad33
79ceadfa9d591934dc9d87125772ae7cf3a0f26fe91ee77f27eb11a23924981f
3d481e4dcd5f0e849323822d687f4cbc7570474b985251fc13cb019ac5f72a81
80a680a0054e1b0968a3ade98a8ccb42789082584d3a72917d2a1857b348b5e6
fd031ad921c6dad12473e6fa148c74e05660e98be91cb68d3c5b43da423d06d5
7f5fa38b7ce6401ff353a9b680fccadc138af24cd66369a05a796c363bcdeb45
8a6ab1effe3736620a7d21ff943e7c50bcd244d9ebd26dd0f613c86b1e866ad2
8020cb196e1830ce33c5242cebae2c13ca64e36b0a1186cda71b3b19fcdcddef
8c8ad9f8f7fd75798017937e117699485711220f2b529f0b4e62c1765461538c
66cad5c567e55c73b672f710a95c05825e26786ba1c69551f0d1c6ef92887d70
02379f8b49c069e1a14e4fc371b84aa46e0589d6b77050a51fa888b20de13a7f
f7bc1ca4d9c8a3ac7dd2c53f3829c459bf656fef033a1c2e07942e573f1488f8
fafd7c0ef34504324578f48f22bdcca6b87d3a57a80ac6a9359b9559ba1ba599
e0f5d057bc2ea677d16506f272387801e5717afad3c27d29c56e6f491496d399
951dbfd636ec1d0a164350cad1f0a69f097ae28447520e6d3ce4dd18cb7ce33e
b1707faa90c8d2853c98f3deb845283b119a3f626ff8733d1ca23367b617e24e
6835f35622b8a7f0a3393cc95aae42dd81d51cc31de0fd70aa0a2c4d9f7dc540
6038a51c740da0a594613a563e0290493e1cea0f45b98334711137ecb309ce1d
9426f1fb57ce97b8a50cdfc98da236cd6c12404e0d9a27dbecfe53bd4e70ecc8
55d02d7484a0e86dd43b28722b91ee46e47af72cddb478692b0569cf0ea19f8d
48a809c673c51d8acd8c8de0877bf5addd82ebd7ae39260dee46627b99e5f35e
61d251ca5cd28071a241e6e31dd1cca9d6c92aeab9bfb705c90fc02fa59d0721
2e129f22a54b5a80cd9993eb6b10c0a8f3eb7abe2b47f5d2cc64c9365a083864
c7acbe0e490196575368ffe3cdb08a12cb99466c0c77423fd1592033591e04ba
193d41b5c3ef5c85851f4ab1b3b5fb8a8f90575e2377362e89e35915de9166c3
eb4c463ca22eac43632f8f5048343457e9bb0bbce83a67c4f3c34a74341622a1
daff5bbe42272e24b29056ea9ab29b5dd70b9aa985370989ff1502d20b715890
22b400207ffd843e7aec2ec2cbd6d53159e7252f1c60314ad5164f903862972b
d4f3bf876293ee9de770ae8a940458d75ed0f392a7fc93682223cee31d257d32
a0ed984f000cf909cba32fddc9c9ac78c626105f2c0bc8abbf3f3f50d312a342
825ca691eeaf5c2373892742050620e26ce1c1e81fb88c8ef089a1fbabb66448
08e2a544c9756812b03513048e7ae109e9989aa87facd534ad633c15e440db49
538d67388d4f47b8ee58da756dfb913f2762610044fc375849a4aeddd9cf8adc
7b11c5921e69b8544ad1fa6ff1f7da57912f1473392a64312b0132e657bf788e
87b0b1cb7b95066f491834c1f3f01afb6bc9e6e95a8a3a7b4cd3177ff0d52f4f
a6746ac64f664e61e3d1d041fc71ca5379c831925b714461b3203187e44a2542
a88bdd638069672b79a708aeea9a12ab17fd7baebfc898182d4df64404dce9ca
0db7d0fb2b4262b93d1ec21bf00a428fafc96fb410cf1d5d2c30a8cefd374e93
8e2218f82bc6b653705ff81ff03dc98dc90dd7249183521e1dae62429fe6e4ad
2d48037c916139c029f0d9ff9bb47ecb5dd4c0e3af9ce746077cb4a16759c62f
a03d1c762fea0edb027741537fbdad74b2aee95828218c8c6358ebe019f14782
711ea5dd1cd0884c949066bc35af77e89a6760fc7812feaf34ff34c14b6cd124
1802816711243b32c875a8b0c5fe04212f9dac427dfaa04a4ae614111cc9d490
11421e2188d4b46d484edc1f346f22fefeba6ed504952fa53b33878512b7f364
a54098f99bf02a7a143ae21a0e05809ee66ba2f712fa9a270c307fde43587f14
0c3fdc1a5c92bba34cc7bd69f84771e7a979601e71428b9e8ff91e7a104552ff
023b3bf5fac6a8518ee80de4b3a41e443cee431eaa6e701b839b2ab5b42420b5
cc073fa309af6fd9023c642316785bbd56d49f4ba60f96248a8991d3287455f3
a24b3cf2b9e1c2639e8dc4a33dcc4f652afe949e8dfe50d3dbe6fb075c63e5ef
454e182ce3b29aa1942470b0a223d6070dfdfad119fbd9cd86d52d34d0261bd1
14287adcbbd45689d25c04cc29edb30f8f2bc8e40499436fa09b33558a8f3c5e
ae957a764471b46c353396802418c1a13f3701b7847ed3749bcb619a95ec5546
500832b21119374cec8b831a06969cb0534892707b3c8b8d49354bc2c9e87262
29a45c4d1a1240f45efb271b7710418501030ece1a526557364f65591d34daf8
2ecc2c67e41643ee4d3a1b1c27d2d608396b68de4293d2df2e061faca97456e6
051f0a7a60641592f2a83846c157bb42538bf0be3f31b27e24f6c69ca6575485
c108938b16d30b2a13bbaa2fea82ddecefd85b3c0e52a0a200f63a7c5c6526c9
5b5c1d60dcef4e90866a824a1dbf3e0cb67eb95661dee5f17aaa146d04c99c8e
eb8517229cccf5d0a5f3f43f21ef18ca68520684f4761fed62894555e05fb132
227566441065679a17fcaae3a15b019606f3ea941b0b78e2fbcff48c0de253ea
61618f5db64098bfbfe1f4a6e05ff647c703a00b74854bc4a5a60dd1672a7e66
44add97c02f2d128a3b15363ccf61c126438aa96c826b2c0577402fa56de3cb7
00feb43366991297b770fe2929bde6d1844e6b984662db62323138662c9b51db
43dac961a766a410c13eb45ff6422d6f9fd69944e1d81801b19bb38a2f30fb54
e8deb99a10dcf354e4065c587c51feaba35d45b6a3cd3333a5d8657ce0e7bc34
f0729cfcd090b1ad620934a99d06adc5705a4bd2d2d57ac2e82a35f5e030eb1d
9cb217fd3190461954921f1b5be6e6d7337cd694ad30d7a3e10f672b2f74a717
346cf7fd86aab5ab941b926a243c6aa7b65fdca52aaddfd6884cc1fd7e0f4a2e
d8ce9c4921d6fdd4c57dc64a250c2dc886e74e9bff336b8c7030ba4a539ff79a
eb67d21bf9fec6d9ee9af47b3dfe95a44c201836d17e7e08a3bfe561908b7008
bdb306831f8bbcd7e675f7db95ef50d66ecff44dabdfe6e89972cf016b915b6b
e3d1aed962e48afacd57007efcfcafb0b4d245ae4c579676d98c1b21855fbf9d
c092a99f788b140c59d0288560c53c6522cffcaa9223c2ce88b8fb8a7ad5ee45
6d0dc8953f306f6913d3be67c54a113eee14ca9600b7d6b5cd99f2ea18cf4984
94bd0d5ee0cc10c95c836337733f1730be33151383d7946afbc24e2e2fabd346
99ffdc3d1b432890b662126c09dc56a45c6491b580dd5bc4b2a7f464fd7b00ed
fb8d38ac9ebfb8f196a6549161931d723151d3aa180f77fe9130e059e12d2481
fca6ae50a370e226998e785ec9c5b7dcfcb23263ee7bb621da05eaa6fcb806e7
f3d0c20bd10b0b2fae3ea4bb4180ae2174310a283c03f5c7733ac937a07198fc
f4cdac0c218c483533fdd15627cb3c93296b714159bbb1594c3c3ea59ba09185
db3b2227ac0212da4c2a01ba24c41aa1a075e42117d70bd220b8f03c27f6602f
9561dc8fad0e5e16c1a035da4edaa1c8255cd36340a0d3ee2731ffa65be2037d
53dc9c6bc9462c30449f4df70c19981d5d144895518bee4258934399f91fc181
ab0bf02e7debd2d7fc43f9e0d3cf5d3c816a9c10398b3336eb17cf6c3ef20f3f
d1666cdfb8645f4f2e5f554a4036e789dc98eadc0d06bf99c7daa4203a0a3e77
30044bd22f2ed02193191fae5ece9a623c7c9b22442fc2f2274da08a5708f0d6