Mở đầu
Là một người ngoại đạo với Machine learning, Nhi chỉ mới bắt đầu (tự) học về nó khoảng 2 năm nay và hằng ngày Nhi vẫn đang cố gắng theo đuổi mục tiêu này với vốn kiến thức ít ỏi của mình (Nhi xuất phát từ phái Thống kê cổ điển). Như một hệ quả tất yếu, sau khi thực hành trên những bài toán đơn giản, người học sẽ dần dần chạm đến những vấn đề thú vị hơn (và phức tạp hơn), thí dụ nhận diện vật thể / trích xuất giá trị định lượng từ ảnh chụp. Bài toán này sử dụng dữ liệu đầu vào là ảnh chụp và cần công cụ là Convolutional neural network (mạng neuron tích chập, CNN).
Gần đây, với những R package như magick (xử lý ảnh chụp), keras (cho phép kết nối với TensorFlow backend từ Python, thậm chí không cần GPU) và lime (giải thích cơ chế mô hình, với ứng dụng cho dữ liệu văn bản/matrix và ảnh chụp), việc thực hiện một mô hình CNN cho các nghiên cứu đơn giản, quy mô vừa và nhỏ là hoàn toàn khả thi.
Tiến sĩ Shirin Glander, một đàn chị trong giới data science tại Đức vừa công bố một thí dụ cho thấy tính khả thi và đơn giản đáng ngạc nhiên khi thực hành Computervision model với keras và lime. Tuy bài viết của chị ấy rất đáng quý,vì có hiệu quả tích cực, khích lệ tinh thần của những người nghiệp dư - tự học trong đó có Nhi; nhưng nội dung của bài mang tích chất biểu diễn, nhưng có thể gây khó hiểu cho nhiều bạn , do đó Nhi quyết định biên tập lại thí dụ minh họa và code của Shirin kèm theo diễn giải bằng tiếng Việt cho các bạn bác sĩ tại VN.
Bước 1: Mô tả bài toán và dữ liệu
Bài toán minh họa mà chúng ta sẽ giải quyết có Mục tiêu là xây dựng một mô hình mạng neuron cho phép nhận dạng 15 loại quả từ một tấm ảnh chụp. Danh sách 15 loại quả này bao gồm: Kiwi, chuối (Banana),Mơ (Apricot),bơ (Avocado),dừa (Cocos), quít (Mandarine), cam (Orange), Chanh xanh (Limes), chanh vàng (Lemon), đào (Peach), mận (Plum), Raspberry, dâu (Strawberry), Dứa (Pineapple) và lựu (Pomegranate).
Bộ dữ liệu dùng để huấn luyện mô hình được lấy về từ kaggle:
https://www.kaggle.com/moltean/fruits/data
Đầu tiên, bạn hãy tải file ZIP của dữ liệu về máy tính của mình, nó có kích thước khoảng 390 Mb. Sau khi giải nén, bạn sẽ có một thư mục tên là fruits-360, với cấu trúc gồm 4 thư mục con bên trong, trong đó ta quan tâm đến 2 thư mục là Test (chứa 12642 ảnh JPG, trong 74 thư mục riêng biệt cho từng loại quả) và Training (chứa 37101 file ảnh JPG, trong 74 thư mục riêng).
Khi thăm dò một vài thư mục bất kì, thí dụ: Straberry, Banana,Golden Apple và Orange, bạn sẽ thấy hàng trăm ảnh chụp bên trong. Mỗi ảnh có kích thước đồng nhất là 100x100 pixels, trình bày hình ảnh của đối tượng (thí dụ quả dâu) nằm tại trung tâm tấm ảnh, trên 1 nền trắng và dưới nhiều góc nhìn khác nhau với đầy đủ những chi tiết đặc thù bao gồm thân, cuống, lá và màu sắc.
Một vài nhận xét của Nhi về bài toán và dữ liệu:
Đây là một bài toán rất đơn giản, vì để giải quyết nó chúng ta không cần kiến thức chuyên ngành nào cả, khác với những vấn đề phức tạp như ảnh CTscan, ảnh giải phẫu bệnh, hay nhân trắc học… Và nhất là, ảnh chụp đã được xử lý đồng nhất về màu sắc, kích thước, dán nhãn rõ ràng, đối tượng cần nhận diện nằm đúng trung tâm và trên 1 nền trắng hoàn hảo. Số lượng ảnh lớn nhưng kích thước nhỏ (100x100) là một lợi thế rất lớn cho phép giải quyết trên một máy tính không có GPU (như của Nhi) với một cấu trúc mạng neuron đơn giản.
Tuy đơn giản, bài toán hết sức thú vị: nó giúp ta hình dung khái quát về cơ chế nhận diện một đối tượng: các loại quả có thể được phân biệt nhờ: Hình dạng của chúng,như cam và Chuối), màu sắc (Dâu và Chanh), chi tiết phụ (lông của quả kiwi, hạt trên quả dâu, lá của quả thơm).Nguy cơ nhầm lẫn hoàn toàn có thể xảy ra (Cam vs quít vs chanh). Tính thú vị còn ở chỗ Hoa quả gợi cho con người cảm giác dễ chịu, khi liên tưởng đến vị ngon và màu sắc của chúng (không phải ngẫu nhiên mà những bài thực hành Deep learning cho ảnh chụp đều liên quan đến những đối tượng vui nhộn, dễ chịu như: nhận diện Mèo, Chó, Món ăn, Hoa)
Trong bài toán này, dữ liệu không phải là một bảng tính với con số và các biến như ta thường thấy, nhưng chúng ta làm việc trực tiếp trên ảnh chụp. Như vậy một trong các công đoạn thực hiện mô hình là chuyển từ ảnh chụp thành dữ liệu phù hợp cho công cụ ta đang sử dụng (keras và TensorFlow).
Bước 2: Chuẩn bị nguồn cung cấp dữ liệu
Trước hết, bạn cần install Python và package keras cho máy tính, bạn có thể làm theo hướng dẫn tại: https://keras.rstudio.com/index.html
library(keras)
Bây giờ, ta bắt đầu sơ chế dữ liệu: Việc đầu tiên cần làm đó là rút gọn danh sách các loại quả cần nhận dạng từ 74 xuống còn 15 mà thôi:
fruit_list <- c("Kiwi","Banana", "Apricot", "Avocado",
"Cocos", "Mandarine", "Orange",
"Limes", "Lemon", "Peach", "Plum", "Raspberry",
"Strawberry", "Pineapple", "Pomegranate")
fruit_list
## [1] "Kiwi" "Banana" "Apricot" "Avocado" "Cocos"
## [6] "Mandarine" "Orange" "Limes" "Lemon" "Peach"
## [11] "Plum" "Raspberry" "Strawberry" "Pineapple" "Pomegranate"
(Trong danh sách này, vấn đề khó nhất là: Phân biệt Cam và Quýt, Kiwi/Dừa/quả bơ)
Một object tên là output_n được chuẩn bị, để chỉ số nhãn kết quả cần phân loại:
output_n <- length(fruit_list)
output_n
## [1] 15
Tiếp theo, ta dự tính sẽ thu nhỏ kích thước ảnh trong quá trình huấn luyện từ 100x100 còn 20x20,ta chuẩn bị 2 tham số: img_width và img_height của tùy chỉnh target_size.
Một tùy chỉnh khác cũng được thiết lập, đó là số kênh màu sắc (khi sử dụng ảnh màu, có 3 kênh tương ứng R,G,B, còn ảnh đen trắng chỉ có 1 kênh, mỗi pixel trong từng kênh có giá trị từ 0:255)
(tác giả Shirin đã rất thông minh khi làm việc một cách thứ bậc - khai báo các tùy chỉnh bên ngoài, từ đó không cần lặp lại chúng khi áp dụng các hàm keras và không sợ nhầm lẫn)
img_width <- 20
img_height <- 20
target_size <- c(img_width, img_height)
channels <- 3
rbind(img_width,img_height)
## [,1]
## img_width 20
## img_height 20
rbind(target_size)
## [,1] [,2]
## target_size 20 20
rbind(channels)
## [,1]
## channels 3
Tiếp theo, Nhi khai báo đường dẫn đến 2 thư mục dùng để huấn luyện và kiểm định mô hình:
train_image_files_path <- "/Deeplearning/fruits-360/Training/"
valid_image_files_path <- "/Deeplearning/fruits-360/Test/"
train_image_files_path
## [1] "/Deeplearning/fruits-360/Training/"
valid_image_files_path
## [1] "/Deeplearning/fruits-360/Test/"
Hàm image_data_generator được dùng để chuyển từ file ảnh JPEG thành dữ liệu tensor cho keras, hàm này còn cho phép sinh mẫu ngẫu nhiên bằng cách biến dạng/gây nhiễu ngẫu nhiên ảnh gốc, tính năng này chưa được dùng đến trong thí dụ này. Tùy chỉnh rescale cho phép chuyển giá trị pixel từ 0:255 thành thang đo 0:1, để thích hợp cho mạng neuron.
train_data_gen <- image_data_generator(rescale = 1/255)
valid_data_gen <- image_data_generator(rescale = 1/255)
Bây giờ ta có thể lấy mẫu một số ảnh chụp ngẫu nhiên từ 1 trong 2 thư mục Training /Test (qua đường dẫn đã khai báo), sau đó chuyển thành dữ liệu tensor (arrays) , với kích thước ảnh = target size đã được khai báo, class_mode là tính chất của nhãn kết quả = categorical (bài toán phân loại), và danh sách nhãn = fruit_list
train_image_array_gen <- flow_images_from_directory(train_image_files_path,
train_data_gen,
target_size = target_size,
class_mode = "categorical",
classes = fruit_list,
seed = 123)
valid_image_array_gen <- flow_images_from_directory(valid_image_files_path,
valid_data_gen,
target_size = target_size,
class_mode = "categorical",
classes = fruit_list,
seed = 321)
Cấu trúc array dữ liệu huấn luyện:
table(factor(train_image_array_gen$classes))%>%barplot(col=pals::brewer.spectral(15))

table(factor(train_image_array_gen$classes))
##
## 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
## 466 490 492 427 490 490 479 490 492 492 447 490 492 490 492
Cấu trúc dữ liệu kiểm định:
table(factor(valid_image_array_gen$classes))%>%barplot(col=pals::brewer.spectral(15))

table(factor(valid_image_array_gen$classes))
##
## 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
## 156 166 164 143 166 166 161 166 164 164 151 166 165 166 164
Nhi sao lưu lại dữ liệu huấn luyện trong 1 file Rdata
fruits_classes_indices <- train_image_array_gen$class_indices
save(fruits_classes_indices,
file = "/Deeplearning/fruits-360/fruits_classes_indices.RData")
Bước tiếp theo, hai tùy chỉnh khác là train_samples và valid_samples hay kích thước của tập huấn luyện/kiểm định được khai báo trước:
train_samples <- train_image_array_gen$n
valid_samples <- valid_image_array_gen$n
rbind(train_samples,valid_samples)
## [,1]
## train_samples 7219
## valid_samples 2428
Tương tự, ta ấn định trước giá trị 2 tùy chỉnh batch_size và epochs (số lượt huấn luyện)
batch_size <- 32
epochs <- 10
rbind(batch_size,epochs)
## [,1]
## batch_size 32
## epochs 10
Bước 3: Xây dựng cấu trúc mạng neuron
Đây là công đoạn chính của quy trình, ta sẽ xây dựng mạng neuron với cấu trúc gồm các lớp như sau:
Đầu tiên là 1 lớp Convolutional tiếp nhận dữ liệu tensor với các thông số: img_width, img_height, channels đã được khai báo ở trên, hàm kích hoạt là RELU;
Lớp ẩn tiếp theo là 1 lớp conv khác
Tiếp theo là 1 lớp pooling
Tiếp theo, 1 lớp flatten nối với 1 lớp dense (100 neuron) và 1 lớp dropout, công dụng của bộ ba này là chuyển giá trị đầu ra của lớp pooling thành vector feature, sau đó tinh chỉnh
Lớp cuối cùng: xuất kết quả , với số neuron đúng bằng số nhãn cần phân loại, với hàm kích hoạt là softmax
Mô hình được compile với hàm loss = categorical_crossentropy và tùy chỉnh optimizer = rmsprop, tiêu chí tối ưu hóa là accuracy.
# initialise model
model <- keras_model_sequential()%>%
# add layers
layer_conv_2d(filter = 32, kernel_size = c(3,3), padding = "same", input_shape = c(img_width, img_height, channels)) %>%
layer_activation("relu") %>%
# Second hidden layer
layer_conv_2d(filter = 16, kernel_size = c(3,3), padding = "same") %>%
layer_activation_leaky_relu(0.5) %>%
layer_batch_normalization() %>%
# Use max pooling
layer_max_pooling_2d(pool_size = c(2,2)) %>%
layer_dropout(0.25) %>%
# Flatten max filtered output into feature vector
# and feed into dense layer
layer_flatten() %>%
layer_dense(100) %>%
layer_activation("relu") %>%
layer_dropout(0.5) %>%
# Outputs from dense layer are projected onto output layer
layer_dense(output_n) %>%
layer_activation("softmax")
# compile
model %>% compile(
loss = "categorical_crossentropy",
optimizer = optimizer_rmsprop(lr = 0.0001, decay = 1e-6),
metrics = "accuracy"
)
model
## Model
## ___________________________________________________________________________
## Layer (type) Output Shape Param #
## ===========================================================================
## conv2d_1 (Conv2D) (None, 20, 20, 32) 896
## ___________________________________________________________________________
## activation_1 (Activation) (None, 20, 20, 32) 0
## ___________________________________________________________________________
## conv2d_2 (Conv2D) (None, 20, 20, 16) 4624
## ___________________________________________________________________________
## leaky_re_lu_1 (LeakyReLU) (None, 20, 20, 16) 0
## ___________________________________________________________________________
## batch_normalization_1 (BatchNorm (None, 20, 20, 16) 64
## ___________________________________________________________________________
## max_pooling2d_1 (MaxPooling2D) (None, 10, 10, 16) 0
## ___________________________________________________________________________
## dropout_1 (Dropout) (None, 10, 10, 16) 0
## ___________________________________________________________________________
## flatten_1 (Flatten) (None, 1600) 0
## ___________________________________________________________________________
## dense_1 (Dense) (None, 100) 160100
## ___________________________________________________________________________
## activation_2 (Activation) (None, 100) 0
## ___________________________________________________________________________
## dropout_2 (Dropout) (None, 100) 0
## ___________________________________________________________________________
## dense_2 (Dense) (None, 15) 1515
## ___________________________________________________________________________
## activation_3 (Activation) (None, 15) 0
## ===========================================================================
## Total params: 167,199
## Trainable params: 167,167
## Non-trainable params: 32
## ___________________________________________________________________________
Bước 4 : Huấn luyện mô hình
Sau khi có cấu trúc mạng CNN, ta bắt đầu thực hiện quy trình huấn luyện cho nó. Quy trình này bắt đầu bằng việc đưa tensor data (train_image_array_gen, được chuẩn bị ở bước 2) vào lớp tiếp nhận dữ liệu, mô hình được huấn luyện 10 lượt để tối ưu hóa accuracy.Diễn tiến được ghi lại trong file fruits_checkpoints.h5 với đường dẫn tùy chọn, và 1 file logs trong thư mục keras (nếu bạn muốn khai thác bằng tensorboard)
hist <- model %>% fit_generator(
# training data
train_image_array_gen,
# epochs
steps_per_epoch = as.integer(train_samples / batch_size),
epochs = epochs,
# validation data
validation_data = valid_image_array_gen,
validation_steps = as.integer(valid_samples / batch_size),
# print progress
verbose = 2,
callbacks = list(
# save best model after every epoch
callback_model_checkpoint("/Deeplearning/fruits-360/keras/fruits_checkpoints.h5", save_best_only = TRUE),
callback_tensorboard(log_dir = "/Deeplearning/fruits-360/keras/logs")
)
)
Do tính chất đơn giản, mô hình được huấn luyện khá nhanh ngay cả khi dùng CPU. Biểu đồ cho thấy sau 10 lượt huấn luyện/kiểm định, giá trị loss giảm dần và accuracy tăng dần,tức là hiệu năng của mô hình đạt đến trạng thái tối ưu
library(tidyverse)
## -- Attaching packages -------------------------------------------------------------- tidyverse 1.2.1 --
## v ggplot2 2.2.1 v purrr 0.2.5
## v tibble 1.4.2 v dplyr 0.7.5
## v tidyr 0.8.1 v stringr 1.3.1
## v readr 1.1.1 v forcats 0.3.0
## -- Conflicts ----------------------------------------------------------------- tidyverse_conflicts() --
## x dplyr::filter() masks stats::filter()
## x dplyr::lag() masks stats::lag()
plot(hist)+theme_bw()

hist
## Trained on NULL samples (batch_size=NULL, epochs=10)
## Final epoch (plot to see history):
## val_loss: 0.02168
## val_acc: 0.9962
## loss: 0.03006
## acc: 0.9923
Bước 5: Sử dụng mô hình và giải thích cơ chế
Đến lúc này, chúng ta đã đi được 80% hành trình, và có trong tay một mô hình CNN có khả năng nhận diện/phân biệt được 15 loại quả khác nhau dựa vào bất kì ảnh chụp mới nào, với điều kiện ảnh phải có nền trắng và chỉ chứa 1 loại quả duy nhất.
Trước hết, Nhi chuẩn bị đường dẫn đến 1 thư mục chứa dữ liệu (ảnh chụp) cần nhận dạng
test_image_files_path <- "/Deeplearning/fruits-360/Test/"
Quy trình cần 2 package là lime và magick
library(lime) # explaning
##
## Attaching package: 'lime'
## The following object is masked from 'package:dplyr':
##
## explain
library(magick) # image processing
## Linking to ImageMagick 6.9.9.14
## Enabled features: cairo, freetype, fftw, ghostscript, lcms, pango, rsvg, webp
## Disabled features: fontconfig, x11
Mục tiêu giả định là nhận dạng 1 quả dâu và và 1 quả cam, hình ảnh được lấy ngẫu nhiên từ internet
img <- image_read('https://nicohit.co.uk/wp-content/uploads/2016/07/Nicohit-Strawberry-460x460.jpg')
img_path <- file.path(test_image_files_path, "Strawberry", 'strawberry.jpg')
image_write(img, img_path)
plot_superpixels(img_path, n_superpixels = 35, weight = 10)

img2 <- image_read('http://astrotarot.net/wp-content/uploads/2015/09/orange-1024x985.jpg')
img_path2 <- file.path(test_image_files_path, "Orange", 'orange.jpg')
image_write(img2, img_path2)
plot_superpixels(img_path2, n_superpixels = 35, weight = 10)

Ta sử dụng thủ thuật tương tự như bước 2, để thu nhỏ kích thước ảnh còn 20x20, và chuyển giá trị pixel về thang đo 0:1, cho toàn bộ tập kiểm định (bao gồm 2 ảnh mới thêm vào)
Sau đó dùng hàm predict_generator cho mô hình ở trên và dữ liệu cần dán nhãn là test_generator, ta có kết quả và lưu nó lại dưới dạng dataframe
test_datagen <- image_data_generator(rescale = 1/255)
test_generator = flow_images_from_directory(
test_image_files_path,
test_datagen,
target_size = c(20, 20),
class_mode = 'categorical')
predictions <- as.data.frame(predict_generator(model, test_generator, steps = 1))
Sau khi kết hợp với danh sách nhãn kết quả fruits_classes_indices, ta có một matrix xác suất cho mỗi nhãn, và tổng kết lại kết quả
load("/Deeplearning/fruits-360/fruits_classes_indices.RData")
fruits_classes_indices_df <- data.frame(indices = unlist(fruits_classes_indices))
fruits_classes_indices_df <- fruits_classes_indices_df[order(fruits_classes_indices_df$indices), , drop = FALSE]
colnames(predictions) <- rownames(fruits_classes_indices_df)
for (i in 1:nrow(predictions)) {
cat(i, ":")
print(unlist(which.max(predictions[i, ])))
}
## 1 :Raspberry
## 12
## 2 :Banana
## 2
## 3 :Pomegranate
## 15
## 4 :Limes
## 8
## 5 :Peach
## 10
## 6 :Pomegranate
## 15
## 7 :Plum
## 11
## 8 :Peach
## 10
## 9 :Lemon
## 9
## 10 :Orange
## 7
## 11 :Lemon
## 9
## 12 :Banana
## 2
## 13 :Limes
## 8
## 14 :Avocado
## 4
## 15 :Avocado
## 4
## 16 :Raspberry
## 12
## 17 :Lemon
## 9
## 18 :Limes
## 8
## 19 :Raspberry
## 12
## 20 :Lemon
## 9
## 21 :Pineapple
## 14
## 22 :Avocado
## 4
## 23 :Peach
## 10
## 24 :Lemon
## 9
## 25 :Avocado
## 4
## 26 :Banana
## 2
## 27 :Kiwi
## 1
## 28 :Avocado
## 4
## 29 :Avocado
## 4
## 30 :Raspberry
## 12
## 31 :Lemon
## 9
## 32 :Lemon
## 9
Để sử dụng được package lime nhằm giải thích cơ chế hoạt động của mô hình, Shirin Glander viết 1 hàm image_prep, nội dung của nó nhằm chuẩn bị dữ liệu - như thu nhỏ kích thước ảnh còn 20x20 pixels, chuyển thang đo của mỗi pixel về 0:1 và chuyển ảnh chụp thành arrays.
Tiếp theo, danh sách nhãn cần phân loại với định dạng long được tạo ra : fruits_classes_indices_l
sau đó hàm lime được dùng với tùy chỉnh gồm đường dẫn đến 2 tấm ảnh mới (Dâu và Cam) cần giải thích , tên mô hình và danh sách nhãn cần phân loại được đặt trong hàm as_classifier nhằm xác định đây là 1 mô hình phân loại, cuối cùng là hàm image_prep
Nội dung kết quả sẽ được lưu lại trong 1 object explainer. Đây là bộ máy dùng để giải thích cơ chế mô hình trên 2 ảnh bằng hàm expain của lime, với tùy chỉnh dữ liệu x=c(đường dẫn ảnh 1 (dâu), đường dẫn ảnh 2 (cam)), n_label = 1 để tập trung vào 1 nhãn duy nhất,số features=20, superpixels (mảnh ghép ) = 35, nền màu trắng
image_prep <- function(x) {
arrays <- lapply(x, function(path) {
img <- image_load(path, target_size = c(20, 20))
x <- image_to_array(img)
x <- reticulate::array_reshape(x, c(1, dim(x)))
x <- x / 255
})
do.call(abind::abind, c(arrays, list(along = 1)))
}
fruits_classes_indices_l <- rownames(fruits_classes_indices_df)
names(fruits_classes_indices_l) <- unlist(fruits_classes_indices)
fruits_classes_indices_l
## 0 1 2 3 4
## "Kiwi" "Banana" "Apricot" "Avocado" "Cocos"
## 5 6 7 8 9
## "Mandarine" "Orange" "Limes" "Lemon" "Peach"
## 10 11 12 13 14
## "Plum" "Raspberry" "Strawberry" "Pineapple" "Pomegranate"
explainer <- lime(c(img_path, img_path2),
as_classifier(model, fruits_classes_indices_l),
image_prep)
explanation <- lime::explain(x=c(img_path, img_path2), explainer,
n_labels = 1, n_features = 20,
n_superpixels = 35, weight = 10,
background = "white")
Ta tách riêng 2 đối tượng để giải thích trực quan:
strawb <- explanation[explanation$case == "strawberry.jpg",]
oran <- explanation[explanation$case == "orange.jpg",]
Kết quả giải thích trực quan có thể trình bày dưới 2 hình thức: Phân lập những vùng ủng hộ cho nhãn kết quả, trên 1 nền xám:
plot_image_explanation(strawb, display = 'block',threshold=0.01)

plot_image_explanation(oran, display = 'block',threshold=0.01)

Theo kết quả này, ta thấy quả dâu được nhận diện chính xác với prob =1, và toàn bộ quả dâu trong ảnh cũng được phân lập như chứng cứ ủng hộ cho kết quả đó.
Ngược lại, quả cam bị nhận diện nhầm lẫn thành quả chanh (Lemon) với prob=0.93, và những vùng dữ liệu dẫn đến kết quả (sai lầm) này bao gồm cuống, lá và hình tròn với da màu cam
Hình thức thứ hai là chồng lắp những mảng/vùng dữ liệu quan trọng trên nền một tấm ảnh gốc
plot_image_explanation(strawb, fill_alpha = 0.7,threshold=0.01)

plot_image_explanation(oran, fill_alpha = 0.7,threshold=0.01)

Kết luận:
Dù đây chỉ là một thí dụ rất đơn giản, chúng ta có thể hy vọng về khả năng thực hiện được những điều phức tạp hơn, vì hành trình dù dài đến đâu cũng bắt đầu bằng 1 bước chân phải không các bạn ?
LS0tDQp0aXRsZTogIkRlZXAgbGVhcm5pbmc6IE5o4bqtbiBkaeG7h24gduG6rXQgdGjhu4MgcXVhIOG6o25oIGNo4bulcCIgDQphdXRob3I6ICJMw6ogTmfhu41jIEto4bqjIE5oaSINCmRhdGU6ICIwMiBUaMOhbmcgNyAyMDE4Ig0Kb3V0cHV0Og0KICBodG1sX2RvY3VtZW50OiANCiAgICBjb2RlX2Rvd25sb2FkOiB0cnVlDQogICAgY29kZV9mb2xkaW5nOiBoaWRlDQogICAgbnVtYmVyX3NlY3Rpb25zOiB5ZXMNCiAgICB0aGVtZTogImRlZmF1bHQiDQogICAgdG9jOiBUUlVFDQogICAgdG9jX2Zsb2F0OiBUUlVFDQotLS0NCg0KYGBge3Igc2V0dXAsaW5jbHVkZT1GQUxTRX0NCmtuaXRyOjpvcHRzX2NodW5rJHNldChlY2hvID0gVFJVRSkNCmBgYA0KDQohW10oa2VyYXNjb212aXMucG5nKQ0KDQojIE3hu58gxJHhuqd1DQoNCkzDoCBt4buZdCBuZ8aw4budaSBuZ2/huqFpIMSR4bqhbyB24bubaSBNYWNoaW5lIGxlYXJuaW5nLCBOaGkgY2jhu4kgbeG7m2kgYuG6r3QgxJHhuqd1ICh04buxKSBo4buNYyB24buBIG7DsyBraG/huqNuZyAyIG7Eg20gbmF5IHbDoCBo4bqxbmcgbmfDoHkgTmhpIHbhuqtuIMSRYW5nIGPhu5EgZ+G6r25nIHRoZW8gxJF14buVaSBt4bulYyB0acOqdSBuw6B5IHbhu5tpIHbhu5FuIGtp4bq/biB0aOG7qWMgw610IOG7j2kgY+G7p2EgbcOsbmggKE5oaSB4deG6pXQgcGjDoXQgdOG7qyBwaMOhaSBUaOG7kW5nIGvDqiBj4buVIMSRaeG7g24pLiBOaMawIG3hu5l0IGjhu4cgcXXhuqMgdOG6pXQgeeG6v3UsIHNhdSBraGkgdGjhu7FjIGjDoG5oIHRyw6puIG5o4buvbmcgYsOgaSB0b8OhbiDEkcahbiBnaeG6o24sIG5nxrDhu51pIGjhu41jIHPhur0gZOG6p24gZOG6p24gY2jhuqFtIMSR4bq/biBuaOG7r25nIHbhuqVuIMSR4buBIHRow7ogduG7iyBoxqFuICh2w6AgcGjhu6ljIHThuqFwIGjGoW4pLCB0aMOtIGThu6Ugbmjhuq1uIGRp4buHbiB24bqtdCB0aOG7gyAvIHRyw61jaCB4deG6pXQgZ2nDoSB0cuG7iyDEkeG7i25oIGzGsOG7o25nIHThu6sg4bqjbmggY2jhu6VwLiBCw6BpIHRvw6FuIG7DoHkgc+G7rSBk4bulbmcgZOG7ryBsaeG7h3UgxJHhuqd1IHbDoG8gbMOgIOG6o25oIGNo4bulcCB2w6AgY+G6p24gY8O0bmcgY+G7pSBsw6AgQ29udm9sdXRpb25hbCBuZXVyYWwgbmV0d29yayAobeG6oW5nIG5ldXJvbiB0w61jaCBjaOG6rXAsIENOTikuIA0KDQpH4bqnbiDEkcOieSwgduG7m2kgbmjhu69uZyBSIHBhY2thZ2UgbmjGsCBtYWdpY2sgKHjhu60gbMO9IOG6o25oIGNo4bulcCksIGtlcmFzIChjaG8gcGjDqXAga+G6v3QgbuG7kWkgduG7m2kgVGVuc29yRmxvdyBiYWNrZW5kIHThu6sgUHl0aG9uLCB0aOG6rW0gY2jDrSBraMO0bmcgY+G6p24gR1BVKSB2w6AgbGltZSAoZ2nhuqNpIHRow61jaCBjxqEgY2jhur8gbcO0IGjDrG5oLCB24bubaSDhu6luZyBk4bulbmcgY2hvIGThu68gbGnhu4d1IHbEg24gYuG6o24vbWF0cml4IHbDoCDhuqNuaCBjaOG7pXApLCB2aeG7h2MgdGjhu7FjIGhp4buHbiBt4buZdCBtw7QgaMOsbmggQ05OIGNobyBjw6FjIG5naGnDqm4gY+G7qXUgxJHGoW4gZ2nhuqNuLCBxdXkgbcO0IHbhu6thIHbDoCBuaOG7jyBsw6AgaG/DoG4gdG/DoG4ga2jhuqMgdGhpLg0KDQpUaeG6v24gc8SpIFNoaXJpbiBHbGFuZGVyLCBt4buZdCDEkcOgbiBjaOG7iyB0cm9uZyBnaeG7m2kgZGF0YSBzY2llbmNlIHThuqFpIMSQ4bupYyB24burYSBjw7RuZyBi4buRIG3hu5l0IHRow60gZOG7pSBjaG8gdGjhuqV5IHTDrW5oIGto4bqjIHRoaSB2w6AgxJHGoW4gZ2nhuqNuIMSRw6FuZyBuZ+G6oWMgbmhpw6puIGtoaSB0aOG7sWMgaMOgbmggQ29tcHV0ZXJ2aXNpb24gbW9kZWwgduG7m2kga2VyYXMgdsOgIGxpbWUuIFR1eSBiw6BpIHZp4bq/dCBj4bunYSBjaOG7iyDhuqV5IHLhuqV0IMSRw6FuZyBxdcO9LHbDrCBjw7MgaGnhu4d1IHF14bqjIHTDrWNoIGPhu7FjLCBraMOtY2ggbOG7hyB0aW5oIHRo4bqnbiBj4bunYSBuaOG7r25nIG5nxrDhu51pIG5naGnhu4dwIGTGsCAtIHThu7EgaOG7jWMgdHJvbmcgxJHDsyBjw7MgTmhpOyBuaMawbmcgbuG7mWkgZHVuZyBj4bunYSBiw6BpIG1hbmcgdMOtY2ggY2jhuqV0IGJp4buDdSBkaeG7hW4sIG5oxrBuZyBjw7MgdGjhu4MgZ8OieSBraMOzIGhp4buDdSBjaG8gbmhp4buBdSBi4bqhbiAsIGRvIMSRw7MgTmhpIHF1eeG6v3QgxJHhu4tuaCBiacOqbiB04bqtcCBs4bqhaSB0aMOtIGThu6UgbWluaCBo4buNYSB2w6AgY29kZSBj4bunYSBTaGlyaW4ga8OobSB0aGVvIGRp4buFbiBnaeG6o2kgYuG6sW5nIHRp4bq/bmcgVmnhu4d0IGNobyBjw6FjIGLhuqFuIGLDoWMgc8SpIHThuqFpIFZOLg0KDQojIELGsOG7m2MgMTogTcO0IHThuqMgYsOgaSB0b8OhbiB2w6AgZOG7ryBsaeG7h3UNCg0KQsOgaSB0b8OhbiBtaW5oIGjhu41hIG3DoCBjaMO6bmcgdGEgc+G6vSBnaeG6o2kgcXV54bq/dCBjw7MgIE3hu6VjIHRpw6p1IGzDoCB4w6J5IGThu7FuZyBt4buZdCBtw7QgaMOsbmggbeG6oW5nIG5ldXJvbiBjaG8gcGjDqXAgbmjhuq1uIGThuqFuZyAxNSBsb+G6oWkgcXXhuqMgdOG7qyBt4buZdCB04bqlbSDhuqNuaCBjaOG7pXAuIERhbmggc8OhY2ggMTUgbG/huqFpIHF14bqjIG7DoHkgYmFvIGfhu5NtOiBLaXdpLCBjaHXhu5FpIChCYW5hbmEpLE3GoSAoQXByaWNvdCksYsahIChBdm9jYWRvKSxk4burYSAoQ29jb3MpLCBxdcOtdCAoTWFuZGFyaW5lKSwgY2FtIChPcmFuZ2UpLCBDaGFuaCB4YW5oIChMaW1lcyksIGNoYW5oIHbDoG5nIChMZW1vbiksIMSRw6BvIChQZWFjaCksIG3huq1uIChQbHVtKSwgUmFzcGJlcnJ5LCBkw6J1IChTdHJhd2JlcnJ5KSwgROG7qWEgKFBpbmVhcHBsZSkgdsOgIGzhu7F1IChQb21lZ3JhbmF0ZSkuDQoNCkLhu5kgZOG7ryBsaeG7h3UgZMO5bmcgxJHhu4MgaHXhuqVuIGx1eeG7h24gbcO0IGjDrG5oIMSRxrDhu6NjIGzhuqV5IHbhu4EgdOG7qyBrYWdnbGU6DQoNCjxodHRwczovL3d3dy5rYWdnbGUuY29tL21vbHRlYW4vZnJ1aXRzL2RhdGE+DQoNCsSQ4bqndSB0acOqbiwgYuG6oW4gaMOjeSB04bqjaSBmaWxlIFpJUCBj4bunYSBk4buvIGxp4buHdSB24buBIG3DoXkgdMOtbmggY+G7p2EgbcOsbmgsIG7DsyBjw7Mga8OtY2ggdGjGsOG7m2Mga2hv4bqjbmcgMzkwIE1iLiBTYXUga2hpIGdp4bqjaSBuw6luLCBi4bqhbiBz4bq9IGPDsyBt4buZdCB0aMawIG3hu6VjIHTDqm4gbMOgIGZydWl0cy0zNjAsIHbhu5tpIGPhuqV1IHRyw7pjIGfhu5NtIDQgdGjGsCBt4bulYyBjb24gYsOqbiB0cm9uZywgdHJvbmcgxJHDsyB0YSBxdWFuIHTDom0gxJHhur9uIDIgdGjGsCBt4bulYyBsw6AgVGVzdCAoY2jhu6lhIDEyNjQyIOG6o25oIEpQRywgdHJvbmcgNzQgdGjGsCBt4bulYyByacOqbmcgYmnhu4d0IGNobyB04burbmcgbG/huqFpIHF14bqjKSB2w6AgVHJhaW5pbmcgKGNo4bupYSAzNzEwMSBmaWxlIOG6o25oIEpQRywgdHJvbmcgNzQgdGjGsCBt4bulYyByacOqbmcpLiANCg0KS2hpIHRoxINtIGTDsiBt4buZdCB2w6BpIHRoxrAgbeG7pWMgYuG6pXQga8OsLCB0aMOtIGThu6U6IFN0cmFiZXJyeSwgQmFuYW5hLEdvbGRlbiBBcHBsZSB2w6AgT3JhbmdlLCBi4bqhbiBz4bq9IHRo4bqleSBow6BuZyB0csSDbSDhuqNuaCBjaOG7pXAgYsOqbiB0cm9uZy4gTeG7l2kg4bqjbmggY8OzIGvDrWNoIHRoxrDhu5tjIMSR4buTbmcgbmjhuqV0IGzDoCAxMDB4MTAwIHBpeGVscywgdHLDrG5oIGLDoHkgaMOsbmgg4bqjbmggY+G7p2EgxJHhu5FpIHTGsOG7o25nICh0aMOtIGThu6UgcXXhuqMgZMOidSkgbuG6sW0gdOG6oWkgdHJ1bmcgdMOibSB04bqlbSDhuqNuaCwgdHLDqm4gMSBu4buBbiB0cuG6r25nIHbDoCBkxrDhu5tpIG5oaeG7gXUgZ8OzYyBuaMOsbiBraMOhYyBuaGF1IHbhu5tpIMSR4bqneSDEkeG7pyBuaOG7r25nIGNoaSB0aeG6v3QgxJHhurdjIHRow7kgYmFvIGfhu5NtIHRow6JuLCBjdeG7kW5nLCBsw6EgdsOgIG3DoHUgc+G6r2MuDQoNCiFbXShpbWFnZXMucG5nKQ0KDQpN4buZdCB2w6BpIG5o4bqtbiB4w6l0IGPhu6dhIE5oaSB24buBIGLDoGkgdG/DoW4gdsOgIGThu68gbGnhu4d1Og0KDQoxKSDEkMOieSBsw6AgbeG7mXQgYsOgaSB0b8OhbiBy4bqldCDEkcahbiBnaeG6o24sIHbDrCDEkeG7gyBnaeG6o2kgcXV54bq/dCBuw7MgY2jDum5nIHRhIGtow7RuZyBj4bqnbiBraeG6v24gdGjhu6ljIGNodXnDqm4gbmfDoG5oIG7DoG8gY+G6oywga2jDoWMgduG7m2kgbmjhu69uZyB24bqlbiDEkeG7gSBwaOG7qWMgdOG6oXAgbmjGsCDhuqNuaCBDVHNjYW4sIOG6o25oIGdp4bqjaSBwaOG6q3UgYuG7h25oLCBoYXkgbmjDom4gdHLhuq9jIGjhu41jLi4uIFbDoCBuaOG6pXQgbMOgLCDhuqNuaCBjaOG7pXAgxJHDoyDEkcaw4bujYyB44butIGzDvSDEkeG7k25nIG5o4bqldCB24buBIG3DoHUgc+G6r2MsIGvDrWNoIHRoxrDhu5tjLCBkw6FuIG5ow6NuIHLDtSByw6BuZywgxJHhu5FpIHTGsOG7o25nIGPhuqduIG5o4bqtbiBkaeG7h24gbuG6sW0gxJHDum5nIHRydW5nIHTDom0gdsOgIHRyw6puIDEgbuG7gW4gdHLhuq9uZyBob8OgbiBo4bqjby4gU+G7kSBsxrDhu6NuZyDhuqNuaCBs4bubbiBuaMawbmcga8OtY2ggdGjGsOG7m2Mgbmjhu48gKDEwMHgxMDApIGzDoCBt4buZdCBs4bujaSB0aOG6vyBy4bqldCBs4bubbiBjaG8gcGjDqXAgZ2nhuqNpIHF1eeG6v3QgdHLDqm4gbeG7mXQgbcOheSB0w61uaCBraMO0bmcgY8OzIEdQVSAobmjGsCBj4bunYSBOaGkpIHbhu5tpIG3hu5l0IGPhuqV1IHRyw7pjIG3huqFuZyBuZXVyb24gxJHGoW4gZ2nhuqNuLg0KDQoyKSBUdXkgxJHGoW4gZ2nhuqNuLCBiw6BpIHRvw6FuIGjhur90IHPhu6ljIHRow7ogduG7izogbsOzIGdpw7pwIHRhIGjDrG5oIGR1bmcga2jDoWkgcXXDoXQgduG7gSBjxqEgY2jhur8gbmjhuq1uIGRp4buHbiBt4buZdCDEkeG7kWkgdMaw4bujbmc6IGPDoWMgbG/huqFpIHF14bqjIGPDsyB0aOG7gyDEkcaw4bujYyBwaMOibiBiaeG7h3Qgbmjhu506IEjDrG5oIGThuqFuZyBj4bunYSBjaMO6bmcsbmjGsCBjYW0gdsOgIENodeG7kWkpLCBtw6B1IHPhuq9jIChEw6J1IHbDoCBDaGFuaCksIGNoaSB0aeG6v3QgcGjhu6UgKGzDtG5nIGPhu6dhIHF14bqjIGtpd2ksIGjhuqF0IHRyw6puIHF14bqjIGTDonUsIGzDoSBj4bunYSBxdeG6oyB0aMahbSkuTmd1eSBjxqEgbmjhuqdtIGzhuqtuIGhvw6BuIHRvw6BuIGPDsyB0aOG7gyB44bqjeSByYSAoQ2FtIHZzIHF1w610IHZzIGNoYW5oKS4gVMOtbmggdGjDuiB24buLIGPDsm4g4bufIGNo4buXIEhvYSBxdeG6oyBn4bujaSBjaG8gY29uIG5nxrDhu51pIGPhuqNtIGdpw6FjIGThu4UgY2jhu4t1LCBraGkgbGnDqm4gdMaw4bufbmcgxJHhur9uIHbhu4sgbmdvbiB2w6AgbcOgdSBz4bqvYyBj4bunYSBjaMO6bmcgKGtow7RuZyBwaOG6o2kgbmfhuqt1IG5oacOqbiBtw6Agbmjhu69uZyBiw6BpIHRo4buxYyBow6BuaCBEZWVwIGxlYXJuaW5nIGNobyDhuqNuaCBjaOG7pXAgxJHhu4F1IGxpw6puIHF1YW4gxJHhur9uIG5o4buvbmcgxJHhu5FpIHTGsOG7o25nIHZ1aSBuaOG7mW4sIGThu4UgY2jhu4t1IG5oxrA6IG5o4bqtbiBkaeG7h24gTcOobywgQ2jDsywgTcOzbiDEg24sIEhvYSkNCg0KMykgVHJvbmcgYsOgaSB0b8OhbiBuw6B5LCBk4buvIGxp4buHdSBraMO0bmcgcGjhuqNpIGzDoCBt4buZdCBi4bqjbmcgdMOtbmggduG7m2kgY29uIHPhu5EgdsOgIGPDoWMgYmnhur9uIG5oxrAgdGEgdGjGsOG7nW5nIHRo4bqleSwgbmjGsG5nIGNow7puZyB0YSBsw6BtIHZp4buHYyB0cuG7sWMgdGnhur9wIHRyw6puIOG6o25oIGNo4bulcC4gTmjGsCB24bqteSBt4buZdCB0cm9uZyBjw6FjIGPDtG5nIMSRb+G6oW4gdGjhu7FjIGhp4buHbiBtw7QgaMOsbmggbMOgIGNodXnhu4NuIHThu6sg4bqjbmggY2jhu6VwIHRow6BuaCBk4buvIGxp4buHdSBwaMO5IGjhu6NwIGNobyBjw7RuZyBj4bulIHRhIMSRYW5nIHPhu60gZOG7pW5nIChrZXJhcyB2w6AgVGVuc29yRmxvdykuIA0KDQojIELGsOG7m2MgMjogQ2h14bqpbiBi4buLIG5ndeG7k24gY3VuZyBj4bqlcCBk4buvIGxp4buHdQ0KDQpUcsaw4bubYyBo4bq/dCwgYuG6oW4gY+G6p24gaW5zdGFsbCBQeXRob24gdsOgIHBhY2thZ2Uga2VyYXMgY2hvIG3DoXkgdMOtbmgsIGLhuqFuIGPDsyB0aOG7gyBsw6BtIHRoZW8gaMaw4bubbmcgZOG6q24gdOG6oWk6IDxodHRwczovL2tlcmFzLnJzdHVkaW8uY29tL2luZGV4Lmh0bWw+DQoNCmBgYHtyfQ0KbGlicmFyeShrZXJhcykNCmBgYA0KDQpCw6J5IGdp4budLCB0YSBi4bqvdCDEkeG6p3Ugc8ahIGNo4bq/IGThu68gbGnhu4d1OiBWaeG7h2MgxJHhuqd1IHRpw6puIGPhuqduIGzDoG0gxJHDsyBsw6AgcsO6dCBn4buNbiBkYW5oIHPDoWNoIGPDoWMgbG/huqFpIHF14bqjIGPhuqduIG5o4bqtbiBk4bqhbmcgdOG7qyA3NCB4deG7kW5nIGPDsm4gMTUgbcOgIHRow7RpOg0KDQpgYGB7cn0NCmZydWl0X2xpc3QgPC0gYygiS2l3aSIsIkJhbmFuYSIsICJBcHJpY290IiwgIkF2b2NhZG8iLCANCiAgICAgICAgICAgICAgICAiQ29jb3MiLCAiTWFuZGFyaW5lIiwgIk9yYW5nZSIsDQogICAgICAgICAgICAgICAgIkxpbWVzIiwgIkxlbW9uIiwgIlBlYWNoIiwgIlBsdW0iLCAiUmFzcGJlcnJ5IiwgDQogICAgICAgICAgICAgICAgIlN0cmF3YmVycnkiLCAiUGluZWFwcGxlIiwgIlBvbWVncmFuYXRlIikNCg0KZnJ1aXRfbGlzdA0KYGBgDQoNCihUcm9uZyBkYW5oIHPDoWNoIG7DoHksIHbhuqVuIMSR4buBIGtow7MgbmjhuqV0IGzDoDogUGjDom4gYmnhu4d0IENhbSB2w6AgUXXDvXQsIEtpd2kvROG7q2EvcXXhuqMgYsahKQ0KDQpN4buZdCBvYmplY3QgdMOqbiBsw6Agb3V0cHV0X24gxJHGsOG7o2MgY2h14bqpbiBi4buLLCDEkeG7gyBjaOG7iSBz4buRIG5ow6NuIGvhur90IHF14bqjIGPhuqduIHBow6JuIGxv4bqhaToNCg0KYGBge3J9DQpvdXRwdXRfbiA8LSBsZW5ndGgoZnJ1aXRfbGlzdCkNCg0Kb3V0cHV0X24NCmBgYA0KDQpUaeG6v3AgdGhlbywgdGEgZOG7sSB0w61uaCBz4bq9IHRodSBuaOG7jyBrw61jaCB0aMaw4bubYyDhuqNuaCB0cm9uZyBxdcOhIHRyw6xuaCBodeG6pW4gbHV54buHbiB04burIDEwMHgxMDAgY8OybiAyMHgyMCx0YSBjaHXhuqluIGLhu4sgMiB0aGFtIHPhu5E6IGltZ193aWR0aCB2w6AgaW1nX2hlaWdodCBj4bunYSB0w7l5IGNo4buJbmggdGFyZ2V0X3NpemUuIA0KDQpN4buZdCB0w7l5IGNo4buJbmgga2jDoWMgY8WpbmcgxJHGsOG7o2MgdGhp4bq/dCBs4bqtcCwgxJHDsyBsw6Agc+G7kSBrw6puaCBtw6B1IHPhuq9jIChraGkgc+G7rSBk4bulbmcg4bqjbmggbcOgdSwgY8OzIDMga8OqbmggdMawxqFuZyDhu6luZyBSLEcsQiwgY8OybiDhuqNuaCDEkWVuIHRy4bqvbmcgY2jhu4kgY8OzIDEga8OqbmgsIG3hu5dpIHBpeGVsIHRyb25nIHThu6tuZyBrw6puaCBjw7MgZ2nDoSB0cuG7iyB04burIDA6MjU1KQ0KDQoodMOhYyBnaeG6oyBTaGlyaW4gxJHDoyBy4bqldCB0aMO0bmcgbWluaCBraGkgbMOgbSB2aeG7h2MgbeG7mXQgY8OhY2ggdGjhu6kgYuG6rWMgLSBraGFpIGLDoW8gY8OhYyB0w7l5IGNo4buJbmggYsOqbiBuZ2/DoGksIHThu6sgxJHDsyBraMO0bmcgY+G6p24gbOG6t3AgbOG6oWkgY2jDum5nIGtoaSDDoXAgZOG7pW5nIGPDoWMgaMOgbSBrZXJhcyB2w6Aga2jDtG5nIHPhu6MgbmjhuqdtIGzhuqtuKQ0KDQpgYGB7cn0NCmltZ193aWR0aCA8LSAyMA0KaW1nX2hlaWdodCA8LSAyMA0KdGFyZ2V0X3NpemUgPC0gYyhpbWdfd2lkdGgsIGltZ19oZWlnaHQpDQoNCmNoYW5uZWxzIDwtIDMNCg0KcmJpbmQoaW1nX3dpZHRoLGltZ19oZWlnaHQpDQoNCnJiaW5kKHRhcmdldF9zaXplKQ0KDQpyYmluZChjaGFubmVscykNCmBgYA0KDQpUaeG6v3AgdGhlbywgTmhpIGtoYWkgYsOhbyDEkcaw4budbmcgZOG6q24gxJHhur9uIDIgdGjGsCBt4bulYyBkw7luZyDEkeG7gyBodeG6pW4gbHV54buHbiB2w6Aga2nhu4NtIMSR4buLbmggbcO0IGjDrG5oOg0KDQpgYGB7cn0NCnRyYWluX2ltYWdlX2ZpbGVzX3BhdGggPC0gIi9EZWVwbGVhcm5pbmcvZnJ1aXRzLTM2MC9UcmFpbmluZy8iDQp2YWxpZF9pbWFnZV9maWxlc19wYXRoIDwtICIvRGVlcGxlYXJuaW5nL2ZydWl0cy0zNjAvVGVzdC8iDQoNCnRyYWluX2ltYWdlX2ZpbGVzX3BhdGgNCnZhbGlkX2ltYWdlX2ZpbGVzX3BhdGgNCmBgYA0KDQpIw6BtIGltYWdlX2RhdGFfZ2VuZXJhdG9yIMSRxrDhu6NjIGTDuW5nIMSR4buDIGNodXnhu4NuIHThu6sgZmlsZSDhuqNuaCBKUEVHIHRow6BuaCBk4buvIGxp4buHdSB0ZW5zb3IgY2hvIGtlcmFzLCBow6BtIG7DoHkgY8OybiBjaG8gcGjDqXAgc2luaCBt4bqrdSBuZ+G6q3Ugbmhpw6puIGLhurFuZyBjw6FjaCBiaeG6v24gZOG6oW5nL2fDonkgbmhp4buFdSBuZ+G6q3Ugbmhpw6puIOG6o25oIGfhu5FjLCB0w61uaCBuxINuZyBuw6B5IGNoxrBhIMSRxrDhu6NjIGTDuW5nIMSR4bq/biB0cm9uZyB0aMOtIGThu6UgbsOgeS4gVMO5eSBjaOG7iW5oIHJlc2NhbGUgY2hvIHBow6lwIGNodXnhu4NuIGdpw6EgdHLhu4sgcGl4ZWwgdOG7qyAwOjI1NSB0aMOgbmggdGhhbmcgxJFvIDA6MSwgxJHhu4MgdGjDrWNoIGjhu6NwIGNobyBt4bqhbmcgbmV1cm9uLg0KDQpgYGB7cn0NCnRyYWluX2RhdGFfZ2VuIDwtIGltYWdlX2RhdGFfZ2VuZXJhdG9yKHJlc2NhbGUgPSAxLzI1NSkNCnZhbGlkX2RhdGFfZ2VuIDwtIGltYWdlX2RhdGFfZ2VuZXJhdG9yKHJlc2NhbGUgPSAxLzI1NSkgDQpgYGANCg0KDQpCw6J5IGdp4budIHRhIGPDsyB0aOG7gyBs4bqleSBt4bqrdSBt4buZdCBz4buRIOG6o25oIGNo4bulcCBuZ+G6q3Ugbmhpw6puIHThu6sgMSB0cm9uZyAyIHRoxrAgbeG7pWMgVHJhaW5pbmcgL1Rlc3QgKHF1YSDEkcaw4budbmcgZOG6q24gxJHDoyBraGFpIGLDoW8pLCBzYXUgxJHDsyBjaHV54buDbiB0aMOgbmggZOG7ryBsaeG7h3UgdGVuc29yIChhcnJheXMpICwgduG7m2kga8OtY2ggdGjGsOG7m2Mg4bqjbmggPSB0YXJnZXQgc2l6ZSDEkcOjIMSRxrDhu6NjIGtoYWkgYsOhbywgY2xhc3NfbW9kZSBsw6AgdMOtbmggY2jhuqV0IGPhu6dhIG5ow6NuIGvhur90IHF14bqjID0gY2F0ZWdvcmljYWwgKGLDoGkgdG/DoW4gcGjDom4gbG/huqFpKSwgdsOgIGRhbmggc8OhY2ggbmjDo24gPSBmcnVpdF9saXN0DQoNCmBgYHtyfQ0KdHJhaW5faW1hZ2VfYXJyYXlfZ2VuIDwtIGZsb3dfaW1hZ2VzX2Zyb21fZGlyZWN0b3J5KHRyYWluX2ltYWdlX2ZpbGVzX3BhdGgsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHRyYWluX2RhdGFfZ2VuLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHRhcmdldF9zaXplID0gdGFyZ2V0X3NpemUsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgY2xhc3NfbW9kZSA9ICJjYXRlZ29yaWNhbCIsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgY2xhc3NlcyA9IGZydWl0X2xpc3QsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc2VlZCA9IDEyMykNCg0KDQp2YWxpZF9pbWFnZV9hcnJheV9nZW4gPC0gZmxvd19pbWFnZXNfZnJvbV9kaXJlY3RvcnkodmFsaWRfaW1hZ2VfZmlsZXNfcGF0aCwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdmFsaWRfZGF0YV9nZW4sDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdGFyZ2V0X3NpemUgPSB0YXJnZXRfc2l6ZSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBjbGFzc19tb2RlID0gImNhdGVnb3JpY2FsIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBjbGFzc2VzID0gZnJ1aXRfbGlzdCwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzZWVkID0gMzIxKQ0KYGBgDQoNCkPhuqV1IHRyw7pjIGFycmF5IGThu68gbGnhu4d1IGh14bqlbiBsdXnhu4duOg0KDQpgYGB7cn0NCnRhYmxlKGZhY3Rvcih0cmFpbl9pbWFnZV9hcnJheV9nZW4kY2xhc3NlcykpJT4lYmFycGxvdChjb2w9cGFsczo6YnJld2VyLnNwZWN0cmFsKDE1KSkNCg0KdGFibGUoZmFjdG9yKHRyYWluX2ltYWdlX2FycmF5X2dlbiRjbGFzc2VzKSkNCmBgYA0KDQpD4bqldSB0csO6YyBk4buvIGxp4buHdSBraeG7g20gxJHhu4tuaDoNCg0KYGBge3J9DQp0YWJsZShmYWN0b3IodmFsaWRfaW1hZ2VfYXJyYXlfZ2VuJGNsYXNzZXMpKSU+JWJhcnBsb3QoY29sPXBhbHM6OmJyZXdlci5zcGVjdHJhbCgxNSkpDQoNCnRhYmxlKGZhY3Rvcih2YWxpZF9pbWFnZV9hcnJheV9nZW4kY2xhc3NlcykpDQpgYGANCg0KTmhpIHNhbyBsxrB1IGzhuqFpIGThu68gbGnhu4d1IGh14bqlbiBsdXnhu4duIHRyb25nIDEgZmlsZSBSZGF0YSANCg0KYGBge3J9DQpmcnVpdHNfY2xhc3Nlc19pbmRpY2VzIDwtIHRyYWluX2ltYWdlX2FycmF5X2dlbiRjbGFzc19pbmRpY2VzDQoNCnNhdmUoZnJ1aXRzX2NsYXNzZXNfaW5kaWNlcywgDQogICAgIGZpbGUgPSAiL0RlZXBsZWFybmluZy9mcnVpdHMtMzYwL2ZydWl0c19jbGFzc2VzX2luZGljZXMuUkRhdGEiKQ0KYGBgDQoNCkLGsOG7m2MgdGnhur9wIHRoZW8sIGhhaSB0w7l5IGNo4buJbmgga2jDoWMgbMOgIHRyYWluX3NhbXBsZXMgdsOgIHZhbGlkX3NhbXBsZXMgaGF5IGvDrWNoIHRoxrDhu5tjIGPhu6dhIHThuq1wIGh14bqlbiBsdXnhu4duL2tp4buDbSDEkeG7i25oIMSRxrDhu6NjIGtoYWkgYsOhbyB0csaw4bubYzoNCg0KYGBge3J9DQp0cmFpbl9zYW1wbGVzIDwtIHRyYWluX2ltYWdlX2FycmF5X2dlbiRuDQoNCnZhbGlkX3NhbXBsZXMgPC0gdmFsaWRfaW1hZ2VfYXJyYXlfZ2VuJG4NCg0KcmJpbmQodHJhaW5fc2FtcGxlcyx2YWxpZF9zYW1wbGVzKQ0KYGBgDQoNClTGsMahbmcgdOG7sSwgdGEg4bqlbiDEkeG7i25oIHRyxrDhu5tjIGdpw6EgdHLhu4sgMiB0w7l5IGNo4buJbmggYmF0Y2hfc2l6ZSB2w6AgZXBvY2hzIChz4buRIGzGsOG7o3QgaHXhuqVuIGx1eeG7h24pDQoNCmBgYHtyfQ0KYmF0Y2hfc2l6ZSA8LSAzMg0KZXBvY2hzIDwtIDEwDQoNCnJiaW5kKGJhdGNoX3NpemUsZXBvY2hzKQ0KYGBgDQoNCiMgQsaw4bubYyAzOiBYw6J5IGThu7FuZyBj4bqldSB0csO6YyBt4bqhbmcgbmV1cm9uDQoNCsSQw6J5IGzDoCBjw7RuZyDEkW/huqFuIGNow61uaCBj4bunYSBxdXkgdHLDrG5oLCB0YSBz4bq9IHjDonkgZOG7sW5nIG3huqFuZyBuZXVyb24gduG7m2kgY+G6pXUgdHLDumMgZ+G7k20gY8OhYyBs4bubcCBuaMawIHNhdToNCg0KMSkgxJDhuqd1IHRpw6puIGzDoCAxIGzhu5twIENvbnZvbHV0aW9uYWwgdGnhur9wIG5o4bqtbiBk4buvIGxp4buHdSB0ZW5zb3IgduG7m2kgY8OhYyB0aMO0bmcgc+G7kTogaW1nX3dpZHRoLCBpbWdfaGVpZ2h0LCBjaGFubmVscyDEkcOjIMSRxrDhu6NjIGtoYWkgYsOhbyDhu58gdHLDqm4sIGjDoG0ga8OtY2ggaG/huqF0IGzDoCBSRUxVOw0KDQoyKSBM4bubcCDhuqluIHRp4bq/cCB0aGVvIGzDoCAxIGzhu5twIGNvbnYga2jDoWMNCg0KMykgVGnhur9wIHRoZW8gbMOgIDEgbOG7m3AgcG9vbGluZw0KDQo0KSBUaeG6v3AgdGhlbywgMSBs4bubcCBmbGF0dGVuIG7hu5FpIHbhu5tpIDEgbOG7m3AgZGVuc2UgKDEwMCBuZXVyb24pIHbDoCAxIGzhu5twIGRyb3BvdXQsIGPDtG5nIGThu6VuZyBj4bunYSBi4buZIGJhIG7DoHkgbMOgIGNodXnhu4NuIGdpw6EgdHLhu4sgxJHhuqd1IHJhIGPhu6dhIGzhu5twIHBvb2xpbmcgdGjDoG5oIHZlY3RvciBmZWF0dXJlLCBzYXUgxJHDsyB0aW5oIGNo4buJbmggDQoNCjUpIEzhu5twIGN14buRaSBjw7luZzogeHXhuqV0IGvhur90IHF14bqjICwgduG7m2kgc+G7kSBuZXVyb24gxJHDum5nIGLhurFuZyBz4buRIG5ow6NuIGPhuqduIHBow6JuIGxv4bqhaSwgduG7m2kgaMOgbSAga8OtY2ggaG/huqF0IGzDoCBzb2Z0bWF4DQoNCk3DtCBow6xuaCDEkcaw4bujYyBjb21waWxlIHbhu5tpIGjDoG0gbG9zcyA9IGNhdGVnb3JpY2FsX2Nyb3NzZW50cm9weSB2w6AgdMO5eSBjaOG7iW5oIG9wdGltaXplciA9IHJtc3Byb3AsIHRpw6p1IGNow60gdOG7kWkgxrB1IGjDs2EgbMOgIGFjY3VyYWN5Lg0KDQpgYGB7cn0NCiMgaW5pdGlhbGlzZSBtb2RlbA0KbW9kZWwgPC0ga2VyYXNfbW9kZWxfc2VxdWVudGlhbCgpJT4lDQogIA0KIyBhZGQgbGF5ZXJzDQogIGxheWVyX2NvbnZfMmQoZmlsdGVyID0gMzIsIGtlcm5lbF9zaXplID0gYygzLDMpLCBwYWRkaW5nID0gInNhbWUiLCBpbnB1dF9zaGFwZSA9IGMoaW1nX3dpZHRoLCBpbWdfaGVpZ2h0LCBjaGFubmVscykpICU+JQ0KICBsYXllcl9hY3RpdmF0aW9uKCJyZWx1IikgJT4lDQogIA0KICAjIFNlY29uZCBoaWRkZW4gbGF5ZXINCiAgbGF5ZXJfY29udl8yZChmaWx0ZXIgPSAxNiwga2VybmVsX3NpemUgPSBjKDMsMyksIHBhZGRpbmcgPSAic2FtZSIpICU+JQ0KICBsYXllcl9hY3RpdmF0aW9uX2xlYWt5X3JlbHUoMC41KSAlPiUNCiAgbGF5ZXJfYmF0Y2hfbm9ybWFsaXphdGlvbigpICU+JQ0KICANCiAgIyBVc2UgbWF4IHBvb2xpbmcNCiAgbGF5ZXJfbWF4X3Bvb2xpbmdfMmQocG9vbF9zaXplID0gYygyLDIpKSAlPiUNCiAgbGF5ZXJfZHJvcG91dCgwLjI1KSAlPiUNCiAgDQogICMgRmxhdHRlbiBtYXggZmlsdGVyZWQgb3V0cHV0IGludG8gZmVhdHVyZSB2ZWN0b3IgDQogICMgYW5kIGZlZWQgaW50byBkZW5zZSBsYXllcg0KICBsYXllcl9mbGF0dGVuKCkgJT4lDQogIGxheWVyX2RlbnNlKDEwMCkgJT4lDQogIGxheWVyX2FjdGl2YXRpb24oInJlbHUiKSAlPiUNCiAgbGF5ZXJfZHJvcG91dCgwLjUpICU+JQ0KICANCiAgIyBPdXRwdXRzIGZyb20gZGVuc2UgbGF5ZXIgYXJlIHByb2plY3RlZCBvbnRvIG91dHB1dCBsYXllcg0KICBsYXllcl9kZW5zZShvdXRwdXRfbikgJT4lIA0KICBsYXllcl9hY3RpdmF0aW9uKCJzb2Z0bWF4IikNCg0KIyBjb21waWxlDQptb2RlbCAlPiUgY29tcGlsZSgNCiAgbG9zcyA9ICJjYXRlZ29yaWNhbF9jcm9zc2VudHJvcHkiLA0KICBvcHRpbWl6ZXIgPSBvcHRpbWl6ZXJfcm1zcHJvcChsciA9IDAuMDAwMSwgZGVjYXkgPSAxZS02KSwNCiAgbWV0cmljcyA9ICJhY2N1cmFjeSINCikNCmBgYA0KDQoNCmBgYHtyfQ0KbW9kZWwNCmBgYA0KDQojIELGsOG7m2MgNCA6IEh14bqlbiBsdXnhu4duIG3DtCBow6xuaA0KDQpTYXUga2hpIGPDsyBj4bqldSB0csO6YyBt4bqhbmcgQ05OLCB0YSBi4bqvdCDEkeG6p3UgdGjhu7FjIGhp4buHbiBxdXkgdHLDrG5oIGh14bqlbiBsdXnhu4duIGNobyBuw7MuIFF1eSB0csOsbmggbsOgeSBi4bqvdCDEkeG6p3UgYuG6sW5nIHZp4buHYyDEkcawYSB0ZW5zb3IgZGF0YSAodHJhaW5faW1hZ2VfYXJyYXlfZ2VuLCDEkcaw4bujYyBjaHXhuqluIGLhu4sg4bufIGLGsOG7m2MgMikgdsOgbyBs4bubcCB0aeG6v3Agbmjhuq1uIGThu68gbGnhu4d1LCBtw7QgaMOsbmggxJHGsOG7o2MgaHXhuqVuIGx1eeG7h24gMTAgbMaw4bujdCDEkeG7gyB04buRaSDGsHUgaMOzYSBhY2N1cmFjeS5EaeG7hW4gdGnhur9uIMSRxrDhu6NjIGdoaSBs4bqhaSB0cm9uZyBmaWxlIGZydWl0c19jaGVja3BvaW50cy5oNSB24bubaSDEkcaw4budbmcgZOG6q24gdMO5eSBjaOG7jW4sIHbDoCAxIGZpbGUgbG9ncyB0cm9uZyB0aMawIG3hu6VjIGtlcmFzIChu4bq/dSBi4bqhbiBtdeG7kW4ga2hhaSB0aMOhYyBi4bqxbmcgdGVuc29yYm9hcmQpDQoNCmBgYHtyfQ0KaGlzdCA8LSBtb2RlbCAlPiUgZml0X2dlbmVyYXRvcigNCiAgIyB0cmFpbmluZyBkYXRhDQogIHRyYWluX2ltYWdlX2FycmF5X2dlbiwNCiAgDQogICMgZXBvY2hzDQogIHN0ZXBzX3Blcl9lcG9jaCA9IGFzLmludGVnZXIodHJhaW5fc2FtcGxlcyAvIGJhdGNoX3NpemUpLCANCiAgZXBvY2hzID0gZXBvY2hzLCANCiAgDQogICMgdmFsaWRhdGlvbiBkYXRhDQogIHZhbGlkYXRpb25fZGF0YSA9IHZhbGlkX2ltYWdlX2FycmF5X2dlbiwNCiAgdmFsaWRhdGlvbl9zdGVwcyA9IGFzLmludGVnZXIodmFsaWRfc2FtcGxlcyAvIGJhdGNoX3NpemUpLA0KICANCiAgIyBwcmludCBwcm9ncmVzcw0KICB2ZXJib3NlID0gMiwNCiAgY2FsbGJhY2tzID0gbGlzdCgNCiAgICAjIHNhdmUgYmVzdCBtb2RlbCBhZnRlciBldmVyeSBlcG9jaA0KICAgIGNhbGxiYWNrX21vZGVsX2NoZWNrcG9pbnQoIi9EZWVwbGVhcm5pbmcvZnJ1aXRzLTM2MC9rZXJhcy9mcnVpdHNfY2hlY2twb2ludHMuaDUiLCBzYXZlX2Jlc3Rfb25seSA9IFRSVUUpLA0KICAgIA0KICAgIGNhbGxiYWNrX3RlbnNvcmJvYXJkKGxvZ19kaXIgPSAiL0RlZXBsZWFybmluZy9mcnVpdHMtMzYwL2tlcmFzL2xvZ3MiKQ0KICApDQopDQpgYGANCg0KRG8gdMOtbmggY2jhuqV0IMSRxqFuIGdp4bqjbiwgbcO0IGjDrG5oIMSRxrDhu6NjIGh14bqlbiBsdXnhu4duIGtow6EgbmhhbmggbmdheSBj4bqjIGtoaSBkw7luZyBDUFUuIEJp4buDdSDEkeG7kyBjaG8gdGjhuqV5IHNhdSAxMCBsxrDhu6N0IGh14bqlbiBsdXnhu4duL2tp4buDbSDEkeG7i25oLCBnacOhIHRy4buLIGxvc3MgZ2nhuqNtIGThuqduIHbDoCBhY2N1cmFjeSB0xINuZyBk4bqnbix04bupYyBsw6AgaGnhu4d1IG7Eg25nIGPhu6dhIG3DtCBow6xuaCDEkeG6oXQgxJHhur9uIHRy4bqhbmcgdGjDoWkgdOG7kWkgxrB1DQoNCmBgYHtyfQ0KDQpsaWJyYXJ5KHRpZHl2ZXJzZSkNCg0KcGxvdChoaXN0KSt0aGVtZV9idygpDQoNCmhpc3QNCmBgYA0KDQojIELGsOG7m2MgNTogU+G7rSBk4bulbmcgbcO0IGjDrG5oIHbDoCBnaeG6o2kgdGjDrWNoIGPGoSBjaOG6vw0KDQrEkOG6v24gbMO6YyBuw6B5LCBjaMO6bmcgdGEgxJHDoyDEkWkgxJHGsOG7o2MgODAlIGjDoG5oIHRyw6xuaCwgdsOgIGPDsyB0cm9uZyB0YXkgbeG7mXQgbcO0IGjDrG5oIENOTiBjw7Mga2jhuqMgbsSDbmcgbmjhuq1uIGRp4buHbi9waMOibiBiaeG7h3QgxJHGsOG7o2MgMTUgbG/huqFpIHF14bqjIGtow6FjIG5oYXUgZOG7sWEgdsOgbyBi4bqldCBrw6wg4bqjbmggY2jhu6VwIG3hu5tpIG7DoG8sIHbhu5tpIMSRaeG7gXUga2nhu4duIOG6o25oIHBo4bqjaSBjw7MgbuG7gW4gdHLhuq9uZyB2w6AgY2jhu4kgY2jhu6lhIDEgbG/huqFpIHF14bqjIGR1eSBuaOG6pXQuDQoNClRyxrDhu5tjIGjhur90LCBOaGkgY2h14bqpbiBi4buLIMSRxrDhu51uZyBk4bqrbiDEkeG6v24gMSB0aMawIG3hu6VjIGNo4bupYSBk4buvIGxp4buHdSAo4bqjbmggY2jhu6VwKSBj4bqnbiBuaOG6rW4gZOG6oW5nDQoNCmBgYHtyfQ0KdGVzdF9pbWFnZV9maWxlc19wYXRoIDwtICIvRGVlcGxlYXJuaW5nL2ZydWl0cy0zNjAvVGVzdC8iDQpgYGANCg0KUXV5IHRyw6xuaCBj4bqnbiAyIHBhY2thZ2UgbMOgIGxpbWUgdsOgIG1hZ2ljaw0KDQpgYGB7cn0NCmxpYnJhcnkobGltZSkgICAgIyBleHBsYW5pbmcNCmxpYnJhcnkobWFnaWNrKSAgIyBpbWFnZSBwcm9jZXNzaW5nDQoNCmBgYA0KDQpN4bulYyB0acOqdSBnaeG6oyDEkeG7i25oIGzDoCBuaOG6rW4gZOG6oW5nIDEgcXXhuqMgZMOidSB2w6AgdsOgIDEgcXXhuqMgY2FtLCBow6xuaCDhuqNuaCDEkcaw4bujYyBs4bqleSBuZ+G6q3Ugbmhpw6puIHThu6sgaW50ZXJuZXQNCg0KYGBge3J9DQppbWcgPC0gaW1hZ2VfcmVhZCgnaHR0cHM6Ly9uaWNvaGl0LmNvLnVrL3dwLWNvbnRlbnQvdXBsb2Fkcy8yMDE2LzA3L05pY29oaXQtU3RyYXdiZXJyeS00NjB4NDYwLmpwZycpDQppbWdfcGF0aCA8LSBmaWxlLnBhdGgodGVzdF9pbWFnZV9maWxlc19wYXRoLCAiU3RyYXdiZXJyeSIsICdzdHJhd2JlcnJ5LmpwZycpDQppbWFnZV93cml0ZShpbWcsIGltZ19wYXRoKQ0KDQpwbG90X3N1cGVycGl4ZWxzKGltZ19wYXRoLCBuX3N1cGVycGl4ZWxzID0gMzUsIHdlaWdodCA9IDEwKQ0KDQppbWcyIDwtIGltYWdlX3JlYWQoJ2h0dHA6Ly9hc3Ryb3Rhcm90Lm5ldC93cC1jb250ZW50L3VwbG9hZHMvMjAxNS8wOS9vcmFuZ2UtMTAyNHg5ODUuanBnJykNCmltZ19wYXRoMiA8LSBmaWxlLnBhdGgodGVzdF9pbWFnZV9maWxlc19wYXRoLCAiT3JhbmdlIiwgJ29yYW5nZS5qcGcnKQ0KaW1hZ2Vfd3JpdGUoaW1nMiwgaW1nX3BhdGgyKQ0KDQpwbG90X3N1cGVycGl4ZWxzKGltZ19wYXRoMiwgbl9zdXBlcnBpeGVscyA9IDM1LCB3ZWlnaHQgPSAxMCkNCmBgYA0KDQpUYSBz4butIGThu6VuZyB0aOG7pyB0aHXhuq10IHTGsMahbmcgdOG7sSBuaMawIGLGsOG7m2MgMiwgxJHhu4MgdGh1IG5o4buPIGvDrWNoIHRoxrDhu5tjIOG6o25oIGPDsm4gMjB4MjAsIHbDoCBjaHV54buDbiBnacOhIHRy4buLIHBpeGVsIHbhu4EgdGhhbmcgxJFvIDA6MSwgY2hvIHRvw6BuIGLhu5kgdOG6rXAga2nhu4NtIMSR4buLbmggKGJhbyBn4buTbSAyIOG6o25oIG3hu5tpIHRow6ptIHbDoG8pDQoNClNhdSDEkcOzIGTDuW5nIGjDoG0gcHJlZGljdF9nZW5lcmF0b3IgY2hvIG3DtCBow6xuaCDhu58gdHLDqm4gdsOgIGThu68gbGnhu4d1IGPhuqduIGTDoW4gbmjDo24gbMOgIHRlc3RfZ2VuZXJhdG9yLCB0YSBjw7Mga+G6v3QgcXXhuqMgdsOgIGzGsHUgbsOzIGzhuqFpIGTGsOG7m2kgZOG6oW5nIGRhdGFmcmFtZQ0KDQpgYGB7cn0NCnRlc3RfZGF0YWdlbiA8LSBpbWFnZV9kYXRhX2dlbmVyYXRvcihyZXNjYWxlID0gMS8yNTUpDQoNCnRlc3RfZ2VuZXJhdG9yID0gZmxvd19pbWFnZXNfZnJvbV9kaXJlY3RvcnkoDQogIHRlc3RfaW1hZ2VfZmlsZXNfcGF0aCwNCiAgdGVzdF9kYXRhZ2VuLA0KICB0YXJnZXRfc2l6ZSA9IGMoMjAsIDIwKSwNCiAgY2xhc3NfbW9kZSA9ICdjYXRlZ29yaWNhbCcpDQoNCnByZWRpY3Rpb25zIDwtIGFzLmRhdGEuZnJhbWUocHJlZGljdF9nZW5lcmF0b3IobW9kZWwsIHRlc3RfZ2VuZXJhdG9yLCBzdGVwcyA9IDEpKQ0KYGBgDQoNClNhdSBraGkga+G6v3QgaOG7o3AgduG7m2kgZGFuaCBzw6FjaCBuaMOjbiBr4bq/dCBxdeG6oyBmcnVpdHNfY2xhc3Nlc19pbmRpY2VzLCB0YSBjw7MgbeG7mXQgbWF0cml4IHjDoWMgc3XhuqV0IGNobyBt4buXaSBuaMOjbiwgdsOgIHThu5VuZyBr4bq/dCBs4bqhaSBr4bq/dCBxdeG6oyANCg0KYGBge3J9DQpsb2FkKCIvRGVlcGxlYXJuaW5nL2ZydWl0cy0zNjAvZnJ1aXRzX2NsYXNzZXNfaW5kaWNlcy5SRGF0YSIpDQoNCmZydWl0c19jbGFzc2VzX2luZGljZXNfZGYgPC0gZGF0YS5mcmFtZShpbmRpY2VzID0gdW5saXN0KGZydWl0c19jbGFzc2VzX2luZGljZXMpKQ0KDQpmcnVpdHNfY2xhc3Nlc19pbmRpY2VzX2RmIDwtIGZydWl0c19jbGFzc2VzX2luZGljZXNfZGZbb3JkZXIoZnJ1aXRzX2NsYXNzZXNfaW5kaWNlc19kZiRpbmRpY2VzKSwgLCBkcm9wID0gRkFMU0VdDQoNCmNvbG5hbWVzKHByZWRpY3Rpb25zKSA8LSByb3duYW1lcyhmcnVpdHNfY2xhc3Nlc19pbmRpY2VzX2RmKQ0KDQpmb3IgKGkgaW4gMTpucm93KHByZWRpY3Rpb25zKSkgew0KICBjYXQoaSwgIjoiKQ0KICBwcmludCh1bmxpc3Qod2hpY2gubWF4KHByZWRpY3Rpb25zW2ksIF0pKSkNCn0NCmBgYA0KDQrEkOG7gyBz4butIGThu6VuZyDEkcaw4bujYyBwYWNrYWdlIGxpbWUgbmjhurFtIGdp4bqjaSB0aMOtY2ggY8ahIGNo4bq/IGhv4bqhdCDEkeG7mW5nIGPhu6dhIG3DtCBow6xuaCwgU2hpcmluIEdsYW5kZXIgdmnhur90IDEgaMOgbSBpbWFnZV9wcmVwLCBu4buZaSBkdW5nIGPhu6dhIG7DsyBuaOG6sW0gY2h14bqpbiBi4buLIGThu68gbGnhu4d1IC0gbmjGsCB0aHUgbmjhu48ga8OtY2ggdGjGsOG7m2Mg4bqjbmggY8OybiAyMHgyMCBwaXhlbHMsIGNodXnhu4NuIHRoYW5nIMSRbyBj4bunYSBt4buXaSBwaXhlbCB24buBIDA6MSAgdsOgIGNodXnhu4NuIOG6o25oIGNo4bulcCB0aMOgbmggYXJyYXlzLg0KDQpUaeG6v3AgdGhlbywgZGFuaCBzw6FjaCBuaMOjbiBj4bqnbiBwaMOibiBsb+G6oWkgduG7m2kgxJHhu4tuaCBk4bqhbmcgbG9uZyDEkcaw4bujYyB04bqhbyByYSA6IGZydWl0c19jbGFzc2VzX2luZGljZXNfbA0KDQpzYXUgxJHDsyBow6BtIGxpbWUgxJHGsOG7o2MgZMO5bmcgduG7m2kgdMO5eSBjaOG7iW5oIGfhu5NtIMSRxrDhu51uZyBk4bqrbiDEkeG6v24gMiB04bqlbSDhuqNuaCBt4bubaSAoRMOidSB2w6AgQ2FtKSBj4bqnbiBnaeG6o2kgdGjDrWNoICwgdMOqbiBtw7QgaMOsbmggdsOgIGRhbmggc8OhY2ggbmjDo24gY+G6p24gcGjDom4gbG/huqFpIMSRxrDhu6NjIMSR4bq3dCB0cm9uZyBow6BtIGFzX2NsYXNzaWZpZXIgbmjhurFtIHjDoWMgxJHhu4tuaCDEkcOieSBsw6AgMSBtw7QgaMOsbmggcGjDom4gbG/huqFpLCBjdeG7kWkgY8O5bmcgbMOgIGjDoG0gaW1hZ2VfcHJlcA0KDQpO4buZaSBkdW5nIGvhur90IHF14bqjIHPhur0gxJHGsOG7o2MgbMawdSBs4bqhaSB0cm9uZyAxIG9iamVjdCBleHBsYWluZXIuIMSQw6J5IGzDoCBi4buZIG3DoXkgZMO5bmcgxJHhu4MgZ2nhuqNpIHRow61jaCBjxqEgY2jhur8gbcO0IGjDrG5oIHRyw6puIDIg4bqjbmggYuG6sW5nIGjDoG0gZXhwYWluIGPhu6dhIGxpbWUsIHbhu5tpIHTDuXkgY2jhu4luaCBk4buvIGxp4buHdSB4PWMoxJHGsOG7nW5nIGThuqtuIOG6o25oIDEgKGTDonUpLCDEkcaw4budbmcgZOG6q24g4bqjbmggMiAoY2FtKSksIG5fbGFiZWwgPSAxIMSR4buDIHThuq1wIHRydW5nIHbDoG8gMSBuaMOjbiBkdXkgbmjhuqV0LHPhu5EgZmVhdHVyZXM9MjAsIHN1cGVycGl4ZWxzICht4bqjbmggZ2jDqXAgKSA9IDM1LCBu4buBbiBtw6B1IHRy4bqvbmcNCg0KYGBge3J9DQppbWFnZV9wcmVwIDwtIGZ1bmN0aW9uKHgpIHsNCiAgYXJyYXlzIDwtIGxhcHBseSh4LCBmdW5jdGlvbihwYXRoKSB7DQogICAgaW1nIDwtIGltYWdlX2xvYWQocGF0aCwgdGFyZ2V0X3NpemUgPSBjKDIwLCAyMCkpDQogICAgeCA8LSBpbWFnZV90b19hcnJheShpbWcpDQogICAgeCA8LSByZXRpY3VsYXRlOjphcnJheV9yZXNoYXBlKHgsIGMoMSwgZGltKHgpKSkNCiAgICB4IDwtIHggLyAyNTUNCiAgfSkNCiAgZG8uY2FsbChhYmluZDo6YWJpbmQsIGMoYXJyYXlzLCBsaXN0KGFsb25nID0gMSkpKQ0KfQ0KDQpmcnVpdHNfY2xhc3Nlc19pbmRpY2VzX2wgPC0gcm93bmFtZXMoZnJ1aXRzX2NsYXNzZXNfaW5kaWNlc19kZikNCm5hbWVzKGZydWl0c19jbGFzc2VzX2luZGljZXNfbCkgPC0gdW5saXN0KGZydWl0c19jbGFzc2VzX2luZGljZXMpDQpmcnVpdHNfY2xhc3Nlc19pbmRpY2VzX2wNCg0KZXhwbGFpbmVyIDwtIGxpbWUoYyhpbWdfcGF0aCwgaW1nX3BhdGgyKSwgDQogICAgICAgICAgICAgICAgICAgYXNfY2xhc3NpZmllcihtb2RlbCwgZnJ1aXRzX2NsYXNzZXNfaW5kaWNlc19sKSwgDQogICAgICAgICAgICAgICAgICAgaW1hZ2VfcHJlcCkNCg0KZXhwbGFuYXRpb24gPC0gbGltZTo6ZXhwbGFpbih4PWMoaW1nX3BhdGgsIGltZ19wYXRoMiksIGV4cGxhaW5lciwgDQogICAgICAgICAgICAgICAgICAgICAgICBuX2xhYmVscyA9IDEsIG5fZmVhdHVyZXMgPSAyMCwNCiAgICAgICAgICAgICAgICAgICAgICAgIG5fc3VwZXJwaXhlbHMgPSAzNSwgd2VpZ2h0ID0gMTAsDQogICAgICAgICAgICAgICAgICAgICAgICBiYWNrZ3JvdW5kID0gIndoaXRlIikNCg0KYGBgDQoNClRhIHTDoWNoIHJpw6puZyAyIMSR4buRaSB0xrDhu6NuZyDEkeG7gyBnaeG6o2kgdGjDrWNoIHRy4buxYyBxdWFuOg0KDQpgYGB7cn0NCnN0cmF3YiA8LSBleHBsYW5hdGlvbltleHBsYW5hdGlvbiRjYXNlID09ICJzdHJhd2JlcnJ5LmpwZyIsXQ0KDQpvcmFuIDwtIGV4cGxhbmF0aW9uW2V4cGxhbmF0aW9uJGNhc2UgPT0gIm9yYW5nZS5qcGciLF0NCmBgYA0KDQpL4bq/dCBxdeG6oyBnaeG6o2kgdGjDrWNoIHRy4buxYyBxdWFuIGPDsyB0aOG7gyB0csOsbmggYsOgeSBkxrDhu5tpIDIgaMOsbmggdGjhu6ljOiBQaMOibiBs4bqtcCBuaOG7r25nIHbDuW5nIOG7p25nIGjhu5kgY2hvIG5ow6NuIGvhur90IHF14bqjLCB0csOqbiAxIG7hu4FuIHjDoW06DQoNCmBgYHtyfQ0KcGxvdF9pbWFnZV9leHBsYW5hdGlvbihzdHJhd2IsIGRpc3BsYXkgPSAnYmxvY2snLHRocmVzaG9sZD0wLjAxKQ0KDQpwbG90X2ltYWdlX2V4cGxhbmF0aW9uKG9yYW4sIGRpc3BsYXkgPSAnYmxvY2snLHRocmVzaG9sZD0wLjAxKQ0KYGBgDQoNClRoZW8ga+G6v3QgcXXhuqMgbsOgeSwgdGEgdGjhuqV5IHF14bqjIGTDonUgxJHGsOG7o2Mgbmjhuq1uIGRp4buHbiBjaMOtbmggeMOhYyB24bubaSBwcm9iID0xLCB2w6AgdG/DoG4gYuG7mSBxdeG6oyBkw6J1IHRyb25nIOG6o25oIGPFqW5nIMSRxrDhu6NjIHBow6JuIGzhuq1wIG5oxrAgY2jhu6luZyBj4bupIOG7p25nIGjhu5kgY2hvIGvhur90IHF14bqjIMSRw7MuDQoNCk5nxrDhu6NjIGzhuqFpLCBxdeG6oyBjYW0gYuG7iyBuaOG6rW4gZGnhu4duIG5o4bqnbSBs4bqrbiB0aMOgbmggcXXhuqMgY2hhbmggKExlbW9uKSB24bubaSBwcm9iPTAuOTMsIHbDoCBuaOG7r25nIHbDuW5nIGThu68gbGnhu4d1IGThuqtuIMSR4bq/biBr4bq/dCBxdeG6oyAoc2FpIGzhuqdtKSBuw6B5IGJhbyBn4buTbSBjdeG7kW5nLCBsw6EgdsOgIGjDrG5oIHRyw7JuIHbhu5tpIGRhIG3DoHUgY2FtDQoNCkjDrG5oIHRo4bupYyB0aOG7qSBoYWkgbMOgIGNo4buTbmcgbOG6r3Agbmjhu69uZyBt4bqjbmcvdsO5bmcgZOG7ryBsaeG7h3UgcXVhbiB0cuG7jW5nIHRyw6puIG7hu4FuIG3hu5l0IHThuqVtIOG6o25oIGfhu5FjDQoNCmBgYHtyfQ0KcGxvdF9pbWFnZV9leHBsYW5hdGlvbihzdHJhd2IsIGZpbGxfYWxwaGEgPSAwLjcsdGhyZXNob2xkPTAuMDEpDQoNCnBsb3RfaW1hZ2VfZXhwbGFuYXRpb24ob3JhbiwgZmlsbF9hbHBoYSA9IDAuNyx0aHJlc2hvbGQ9MC4wMSkNCmBgYA0KDQojIEvhur90IGx14bqtbjoNCg0KRMO5IMSRw6J5IGNo4buJIGzDoCBt4buZdCB0aMOtIGThu6UgcuG6pXQgxJHGoW4gZ2nhuqNuLCBjaMO6bmcgdGEgY8OzIHRo4buDIGh5IHbhu41uZyB24buBIGto4bqjIG7Eg25nIHRo4buxYyBoaeG7h24gxJHGsOG7o2Mgbmjhu69uZyDEkWnhu4F1IHBo4bupYyB04bqhcCBoxqFuLCB2w6wgaMOgbmggdHLDrG5oIGTDuSBkw6BpIMSR4bq/biDEkcOidSBjxaluZyBi4bqvdCDEkeG6p3UgYuG6sW5nIDEgYsaw4bubYyBjaMOibiBwaOG6o2kga2jDtG5nIGPDoWMgYuG6oW4gPw0K