diff --git a/api/types.go b/api/types.go index 82caf17dc..a0acd6641 100644 --- a/api/types.go +++ b/api/types.go @@ -436,6 +436,7 @@ type ToolProperty struct { Description string `json:"description,omitempty"` Enum []any `json:"enum,omitempty"` Properties *ToolPropertiesMap `json:"properties,omitempty"` + Required []string `json:"required,omitempty"` } // ToTypeScriptType converts a ToolProperty to a TypeScript type string diff --git a/cmd/bench/bench.go b/cmd/bench/bench.go index d6ea0ade2..1ecd42cfc 100644 --- a/cmd/bench/bench.go +++ b/cmd/bench/bench.go @@ -32,6 +32,7 @@ type flagOptions struct { verbose *bool warmup *int promptTokens *int + numCtx *int } type Metrics struct { @@ -48,6 +49,7 @@ type ModelInfo struct { Family string SizeBytes int64 VRAMBytes int64 + NumCtx int64 } const DefaultPrompt = `Please write a descriptive story about a llama named Alonso who grows up to be President of the Land of Llamas. Include details about Alonso's childhood, adolescent years, and how he grew up to be a political mover and shaker. Write the story with a sense of whimsy.` @@ -64,9 +66,12 @@ var promptWordList = []string{ "old", "stone", "bridge", "that", "crosses", "winding", "river", } +// tokensPerWord is the calibrated ratio of tokens to words for the current model. +// Initialized with a heuristic, then updated during warmup based on actual tokenization. +var tokensPerWord = 1.3 + func generatePromptForTokenCount(targetTokens int, epoch int) string { - // ~1.3 tokens per word heuristic - targetWords := int(float64(targetTokens) / 1.3) + targetWords := int(float64(targetTokens) / tokensPerWord) if targetWords < 1 { targetWords = 1 } @@ -81,6 +86,17 @@ func generatePromptForTokenCount(targetTokens int, epoch int) string { return strings.Join(words, " ") } +// calibratePromptTokens adjusts tokensPerWord based on actual tokenization from a warmup run. +func calibratePromptTokens(targetTokens, actualTokens, wordCount int) { + if actualTokens <= 0 || wordCount <= 0 { + return + } + tokensPerWord = float64(actualTokens) / float64(wordCount) + newWords := int(float64(targetTokens) / tokensPerWord) + fmt.Fprintf(os.Stderr, "bench: calibrated %.2f tokens/word (target=%d, got=%d, words=%d → %d)\n", + tokensPerWord, targetTokens, actualTokens, wordCount, newWords) +} + func buildGenerateRequest(model string, fOpt flagOptions, imgData api.ImageData, epoch int) *api.GenerateRequest { options := make(map[string]interface{}) if *fOpt.maxTokens > 0 { @@ -90,6 +106,9 @@ func buildGenerateRequest(model string, fOpt flagOptions, imgData api.ImageData, if fOpt.seed != nil && *fOpt.seed > 0 { options["seed"] = *fOpt.seed } + if fOpt.numCtx != nil && *fOpt.numCtx > 0 { + options["num_ctx"] = *fOpt.numCtx + } var keepAliveDuration *api.Duration if *fOpt.keepAlive > 0 { @@ -146,7 +165,6 @@ func fetchMemoryUsage(ctx context.Context, client *api.Client, model string) (si return m.Size, m.SizeVRAM } } - // Try prefix match (model names may include :latest or tags) for _, m := range resp.Models { if strings.HasPrefix(m.Name, model) || strings.HasPrefix(m.Model, model) { return m.Size, m.SizeVRAM @@ -155,6 +173,19 @@ func fetchMemoryUsage(ctx context.Context, client *api.Client, model string) (si return 0, 0 } +func fetchContextLength(ctx context.Context, client *api.Client, model string) int64 { + resp, err := client.ListRunning(ctx) + if err != nil { + return 0 + } + for _, m := range resp.Models { + if m.Name == model || m.Model == model || strings.HasPrefix(m.Name, model) || strings.HasPrefix(m.Model, model) { + return int64(m.ContextLength) + } + } + return 0 +} + func outputFormatHeader(w io.Writer, format string, verbose bool) { switch format { case "benchstat": @@ -177,8 +208,12 @@ func outputModelInfo(w io.Writer, format string, info ModelInfo) { if info.SizeBytes > 0 { memStr = fmt.Sprintf(" | Size: %d | VRAM: %d", info.SizeBytes, info.VRAMBytes) } - fmt.Fprintf(w, "# Model: %s | Params: %s | Quant: %s | Family: %s%s\n", - info.Name, params, quant, family, memStr) + ctxStr := "" + if info.NumCtx > 0 { + ctxStr = fmt.Sprintf(" | NumCtx: %d", info.NumCtx) + } + fmt.Fprintf(w, "# Model: %s | Params: %s | Quant: %s | Family: %s%s%s\n", + info.Name, params, quant, family, memStr, ctxStr) } func OutputMetrics(w io.Writer, format string, metrics []Metrics, verbose bool) { @@ -276,21 +311,38 @@ func BenchmarkModel(fOpt flagOptions) error { req := buildGenerateRequest(model, fOpt, imgData, -(i + 1)) ctx, cancel := context.WithTimeout(context.Background(), time.Duration(*fOpt.timeout)*time.Second) + var warmupMetrics *api.Metrics err = client.Generate(ctx, req, func(resp api.GenerateResponse) error { + if resp.Done { + warmupMetrics = &resp.Metrics + } return nil }) cancel() if err != nil { fmt.Fprintf(os.Stderr, "WARNING: Warmup %d/%d for %s failed: %v\n", i+1, *fOpt.warmup, model, err) - } else if *fOpt.debug { - fmt.Fprintf(os.Stderr, "Warmup %d/%d for %s complete\n", i+1, *fOpt.warmup, model) + } else { + if *fOpt.debug { + fmt.Fprintf(os.Stderr, "Warmup %d/%d for %s complete\n", i+1, *fOpt.warmup, model) + } + // Calibrate prompt token count on last warmup run + if i == *fOpt.warmup-1 && *fOpt.promptTokens > 0 && warmupMetrics != nil { + prompt := generatePromptForTokenCount(*fOpt.promptTokens, -(i + 1)) + wordCount := len(strings.Fields(prompt)) + calibratePromptTokens(*fOpt.promptTokens, warmupMetrics.PromptEvalCount, wordCount) + } } } - // Fetch memory usage once after warmup (model is loaded and stable) + // Fetch memory/context info once after warmup (model is loaded and stable) memCtx, memCancel := context.WithTimeout(context.Background(), 5*time.Second) info.SizeBytes, info.VRAMBytes = fetchMemoryUsage(memCtx, client, model) + if fOpt.numCtx != nil && *fOpt.numCtx > 0 { + info.NumCtx = int64(*fOpt.numCtx) + } else { + info.NumCtx = fetchContextLength(memCtx, client, model) + } memCancel() outputModelInfo(out, *fOpt.format, info) @@ -479,6 +531,7 @@ func main() { debug: flag.Bool("debug", false, "Show debug information"), warmup: flag.Int("warmup", 1, "Number of warmup requests before timing"), promptTokens: flag.Int("prompt-tokens", 0, "Generate prompt targeting ~N tokens (0 = use -p prompt)"), + numCtx: flag.Int("num-ctx", 0, "Context size (0 = server default)"), } flag.Usage = func() { diff --git a/cmd/cmd.go b/cmd/cmd.go index 79a604fd4..a9bf3d4dc 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -695,7 +695,8 @@ func RunHandler(cmd *cobra.Command, args []string) error { return err } - opts.MultiModal = slices.Contains(info.Capabilities, model.CapabilityVision) + audioCapable := slices.Contains(info.Capabilities, model.CapabilityAudio) + opts.MultiModal = slices.Contains(info.Capabilities, model.CapabilityVision) || audioCapable // TODO: remove the projector info and vision info checks below, // these are left in for backwards compatibility with older servers @@ -1494,6 +1495,9 @@ type displayResponseState struct { func displayResponse(content string, wordWrap bool, state *displayResponseState) { termWidth, _, _ := term.GetSize(int(os.Stdout.Fd())) + if termWidth == 0 { + termWidth = 80 + } if wordWrap && termWidth >= 10 { for _, ch := range content { if state.lineLength+1 > termWidth-5 { diff --git a/cmd/interactive.go b/cmd/interactive.go index cf922d130..bbcff047f 100644 --- a/cmd/interactive.go +++ b/cmd/interactive.go @@ -47,7 +47,7 @@ func generateInteractive(cmd *cobra.Command, opts runOptions) error { fmt.Fprintln(os.Stderr, "Use \"\"\" to begin a multi-line message.") if opts.MultiModal { - fmt.Fprintf(os.Stderr, "Use %s to include .jpg, .png, or .webp images.\n", filepath.FromSlash("/path/to/file")) + fmt.Fprintf(os.Stderr, "Use %s to include .jpg, .png, .webp images, or .wav audio files.\n", filepath.FromSlash("/path/to/file")) } fmt.Fprintln(os.Stderr, "") @@ -592,7 +592,7 @@ func extractFileNames(input string) []string { // Regex to match file paths starting with optional drive letter, / ./ \ or .\ and include escaped or unescaped spaces (\ or %20) // and followed by more characters and a file extension // This will capture non filename strings, but we'll check for file existence to remove mismatches - regexPattern := `(?:[a-zA-Z]:)?(?:\./|/|\\)[\S\\ ]+?\.(?i:jpg|jpeg|png|webp)\b` + regexPattern := `(?:[a-zA-Z]:)?(?:\./|/|\\)[\S\\ ]+?\.(?i:jpg|jpeg|png|webp|wav)\b` re := regexp.MustCompile(regexPattern) return re.FindAllString(input, -1) @@ -608,10 +608,16 @@ func extractFileData(input string) (string, []api.ImageData, error) { if errors.Is(err, os.ErrNotExist) { continue } else if err != nil { - fmt.Fprintf(os.Stderr, "Couldn't process image: %q\n", err) + fmt.Fprintf(os.Stderr, "Couldn't process file: %q\n", err) return "", imgs, err } - fmt.Fprintf(os.Stderr, "Added image '%s'\n", nfp) + ext := strings.ToLower(filepath.Ext(nfp)) + switch ext { + case ".wav": + fmt.Fprintf(os.Stderr, "Added audio '%s'\n", nfp) + default: + fmt.Fprintf(os.Stderr, "Added image '%s'\n", nfp) + } input = strings.ReplaceAll(input, "'"+nfp+"'", "") input = strings.ReplaceAll(input, "'"+fp+"'", "") input = strings.ReplaceAll(input, fp, "") @@ -685,9 +691,9 @@ func getImageData(filePath string) ([]byte, error) { } contentType := http.DetectContentType(buf) - allowedTypes := []string{"image/jpeg", "image/jpg", "image/png", "image/webp"} + allowedTypes := []string{"image/jpeg", "image/jpg", "image/png", "image/webp", "audio/wave"} if !slices.Contains(allowedTypes, contentType) { - return nil, fmt.Errorf("invalid image type: %s", contentType) + return nil, fmt.Errorf("invalid file type: %s", contentType) } info, err := file.Stat() @@ -695,8 +701,7 @@ func getImageData(filePath string) ([]byte, error) { return nil, err } - // Check if the file size exceeds 100MB - var maxSize int64 = 100 * 1024 * 1024 // 100MB in bytes + var maxSize int64 = 100 * 1024 * 1024 // 100MB if info.Size() > maxSize { return nil, errors.New("file size exceeds maximum limit (100MB)") } diff --git a/cmd/interactive_test.go b/cmd/interactive_test.go index 809f53ff9..7a042d77d 100644 --- a/cmd/interactive_test.go +++ b/cmd/interactive_test.go @@ -84,3 +84,33 @@ func TestExtractFileDataRemovesQuotedFilepath(t *testing.T) { assert.Len(t, imgs, 1) assert.Equal(t, cleaned, "before after") } + +func TestExtractFileDataWAV(t *testing.T) { + dir := t.TempDir() + fp := filepath.Join(dir, "sample.wav") + data := make([]byte, 600) + copy(data[:44], []byte{ + 'R', 'I', 'F', 'F', + 0x58, 0x02, 0x00, 0x00, // file size - 8 + 'W', 'A', 'V', 'E', + 'f', 'm', 't', ' ', + 0x10, 0x00, 0x00, 0x00, // fmt chunk size + 0x01, 0x00, // PCM + 0x01, 0x00, // mono + 0x80, 0x3e, 0x00, 0x00, // 16000 Hz + 0x00, 0x7d, 0x00, 0x00, // byte rate + 0x02, 0x00, // block align + 0x10, 0x00, // 16-bit + 'd', 'a', 't', 'a', + 0x34, 0x02, 0x00, 0x00, // data size + }) + if err := os.WriteFile(fp, data, 0o600); err != nil { + t.Fatalf("failed to write test audio: %v", err) + } + + input := "before " + fp + " after" + cleaned, imgs, err := extractFileData(input) + assert.NoError(t, err) + assert.Len(t, imgs, 1) + assert.Equal(t, "before after", cleaned) +} diff --git a/convert/convert.go b/convert/convert.go index 2d2769022..876ac54c0 100644 --- a/convert/convert.go +++ b/convert/convert.go @@ -290,6 +290,8 @@ func LoadModelMetadata(fsys fs.FS) (ModelKV, *Tokenizer, error) { conv = &gemma3Model{Architecture: p.Architectures[0]} case "Gemma3nForConditionalGeneration": conv = &gemma3nModel{} + case "Gemma4ForCausalLM", "Gemma4ForConditionalGeneration": + conv = &gemma4Model{Architecture: p.Architectures[0]} case "Phi3ForCausalLM": conv = &phi3Model{} case "Qwen2ForCausalLM": diff --git a/convert/convert_gemma4.go b/convert/convert_gemma4.go new file mode 100644 index 000000000..6af6951b1 --- /dev/null +++ b/convert/convert_gemma4.go @@ -0,0 +1,574 @@ +package convert + +import ( + "bytes" + "encoding/binary" + "fmt" + "math" + "slices" + "strings" + + "github.com/ollama/ollama/fs/ggml" +) + +type gemma4Model struct { + gemmaModel + Architecture string + TextModel struct { + HiddenSize uint32 `json:"hidden_size"` + NumHiddenLayers uint32 `json:"num_hidden_layers"` + IntermediateSize uint32 `json:"intermediate_size"` + NumAttentionHeads uint32 `json:"num_attention_heads"` + NumKeyValueHeads uint32 `json:"num_key_value_heads"` + HeadDim uint32 `json:"head_dim"` + GlobalHeadDim uint32 `json:"global_head_dim"` + VocabSize uint32 `json:"vocab_size"` + RMSNormEps float32 `json:"rms_norm_eps"` + MaxPositionEmbeddings uint32 `json:"max_position_embeddings"` + SlidingWindow uint32 `json:"sliding_window"` + SlidingWindowPattern *int32 `json:"_sliding_window_pattern"` + LayerTypes []string `json:"layer_types"` + FinalLogitSoftcapping float32 `json:"final_logit_softcapping"` + EnableMoeBlock bool `json:"enable_moe_block"` + NumExperts *uint32 `json:"num_experts"` + TopKExperts *uint32 `json:"top_k_experts"` + ExpertIntermediateSize *uint32 `json:"moe_intermediate_size"` + HiddenSizePerLayerInput *uint32 `json:"hidden_size_per_layer_input"` + NumKVSharedLayers uint32 `json:"num_kv_shared_layers"` + AttentionKEqV bool `json:"attention_k_eq_v"` + NumGlobalKeyValueHeads *uint32 `json:"num_global_key_value_heads"` + QueryPreAttnScalar *uint32 `json:"query_pre_attn_scalar"` + UseDoubleWideMLP bool `json:"use_double_wide_mlp"` + RopeParameters map[string]*struct { + RopeTheta float32 `json:"rope_theta"` + PartialRotaryFactor *float32 `json:"partial_rotary_factor"` + } `json:"rope_parameters"` + } `json:"text_config"` + + VisionModel struct { + HiddenSize uint32 `json:"hidden_size"` + NumHiddenLayers uint32 `json:"num_hidden_layers"` + NumAttentionHeads uint32 `json:"num_attention_heads"` + IntermediateSize uint32 `json:"intermediate_size"` + PatchSize uint32 `json:"patch_size"` + NumChannels uint32 `json:"num_channels"` + PoolingKernelSize uint32 `json:"pooling_kernel_size"` + LayerNormEps float32 `json:"layer_norm_eps"` + } `json:"vision_config"` + + AudioModel *struct { + HiddenSize uint32 `json:"hidden_size"` + OutputProjDims uint32 `json:"output_proj_dims"` + NumHiddenLayers uint32 `json:"num_hidden_layers"` + NumAttentionHeads uint32 `json:"num_attention_heads"` + ConvKernelSize uint32 `json:"conv_kernel_size"` + RMSNormEps float32 `json:"rms_norm_eps"` + } `json:"audio_config"` +} + +func (p *gemma4Model) KV(t *Tokenizer) KV { + kv := p.ModelParameters.KV(t) + kv["general.architecture"] = "gemma4" + kv["tokenizer.ggml.model"] = "llama" + kv["tokenizer.ggml.pre"] = "gemma4" + + tc := p.TextModel + + kv["gemma4.block_count"] = tc.NumHiddenLayers + kv["gemma4.embedding_length"] = tc.HiddenSize + + // Per-layer FFN width: when use_double_wide_mlp is set, KV-shared layers get 2x FFN width. + if tc.UseDoubleWideMLP && tc.NumKVSharedLayers > 0 { + firstShared := int(tc.NumHiddenLayers) - int(tc.NumKVSharedLayers) + ffnWidths := make([]int32, tc.NumHiddenLayers) + for i := range ffnWidths { + if i >= firstShared { + ffnWidths[i] = int32(tc.IntermediateSize * 2) + } else { + ffnWidths[i] = int32(tc.IntermediateSize) + } + } + kv["gemma4.feed_forward_length"] = ffnWidths + } else { + kv["gemma4.feed_forward_length"] = tc.IntermediateSize + } + kv["gemma4.context_length"] = tc.MaxPositionEmbeddings + kv["gemma4.attention.head_count"] = tc.NumAttentionHeads + // Per-layer KV head count array: SWA layers use NumKeyValueHeads, global layers use NumGlobalKeyValueHeads + if tc.NumGlobalKeyValueHeads != nil && *tc.NumGlobalKeyValueHeads != tc.NumKeyValueHeads && len(tc.LayerTypes) > 0 { + kvHeads := make([]int32, len(tc.LayerTypes)) + for i, lt := range tc.LayerTypes { + if lt == "sliding_attention" { + kvHeads[i] = int32(tc.NumKeyValueHeads) + } else { + kvHeads[i] = int32(*tc.NumGlobalKeyValueHeads) + } + } + kv["gemma4.attention.head_count_kv"] = kvHeads + } else { + kv["gemma4.attention.head_count_kv"] = tc.NumKeyValueHeads + } + // key_length = global head dim, key_length_swa = local (SWA) head dim + kv["gemma4.attention.key_length"] = tc.GlobalHeadDim + kv["gemma4.attention.value_length"] = tc.GlobalHeadDim + kv["gemma4.attention.key_length_swa"] = tc.HeadDim + kv["gemma4.attention.value_length_swa"] = tc.HeadDim + kv["gemma4.attention.layer_norm_rms_epsilon"] = tc.RMSNormEps + kv["gemma4.attention.sliding_window"] = tc.SlidingWindow + + // Sliding window pattern from layer_types + if len(tc.LayerTypes) > 0 { + kv["gemma4.attention.sliding_window_pattern"] = slices.Collect(func(yield func(bool) bool) { + for _, lt := range tc.LayerTypes { + if !yield(lt == "sliding_attention") { + break + } + } + }) + } + + kv["gemma4.attention.shared_kv_layers"] = tc.NumKVSharedLayers + + // RoPE: dimension_count is the full global head dim (freq_factors handle partial rotation) + if rp, ok := tc.RopeParameters["full_attention"]; ok && rp != nil { + kv["gemma4.rope.freq_base"] = rp.RopeTheta + kv["gemma4.rope.dimension_count"] = tc.GlobalHeadDim + } + if rp, ok := tc.RopeParameters["sliding_attention"]; ok && rp != nil { + kv["gemma4.rope.freq_base_swa"] = rp.RopeTheta + kv["gemma4.rope.dimension_count_swa"] = tc.HeadDim + } + + if tc.FinalLogitSoftcapping > 0 { + kv["gemma4.final_logit_softcapping"] = tc.FinalLogitSoftcapping + } + + // MoE + if tc.EnableMoeBlock && tc.NumExperts != nil { + kv["gemma4.expert_count"] = *tc.NumExperts + if tc.TopKExperts != nil { + kv["gemma4.expert_used_count"] = *tc.TopKExperts + } + if tc.ExpertIntermediateSize != nil { + kv["gemma4.expert_feed_forward_length"] = *tc.ExpertIntermediateSize + } + } + + // PLE — always emit, even when 0 + pleSize := uint32(0) + if tc.HiddenSizePerLayerInput != nil { + pleSize = *tc.HiddenSizePerLayerInput + } + kv["gemma4.embedding_length_per_layer_input"] = pleSize + + // Vision model KV metadata + vc := p.VisionModel + if vc.NumHiddenLayers > 0 { + kv["gemma4.vision.block_count"] = vc.NumHiddenLayers + kv["gemma4.vision.embedding_length"] = vc.HiddenSize + kv["gemma4.vision.attention.head_count"] = vc.NumAttentionHeads + kv["gemma4.vision.feed_forward_length"] = vc.IntermediateSize + kv["gemma4.vision.patch_size"] = vc.PatchSize + numCh := vc.NumChannels + if numCh == 0 { + numCh = 3 + } + kv["gemma4.vision.num_channels"] = numCh + nMerge := vc.PoolingKernelSize + if nMerge == 0 { + nMerge = 3 + } + kv["gemma4.vision.projector.scale_factor"] = nMerge + eps := vc.LayerNormEps + if eps == 0 { + eps = 1e-6 + } + kv["gemma4.vision.attention.layer_norm_epsilon"] = eps + } + + // Audio model KV metadata + if p.AudioModel != nil && p.AudioModel.NumHiddenLayers > 0 { + ac := p.AudioModel + kv["gemma4.audio.block_count"] = ac.NumHiddenLayers + kv["gemma4.audio.embedding_length"] = ac.HiddenSize + kv["gemma4.audio.feed_forward_length"] = ac.HiddenSize * 4 + kv["gemma4.audio.attention.head_count"] = ac.NumAttentionHeads + eps := ac.RMSNormEps + if eps == 0 { + eps = 1e-6 + } + kv["gemma4.audio.attention.layer_norm_epsilon"] = eps + if ac.ConvKernelSize > 0 { + kv["gemma4.audio.conv_kernel_size"] = ac.ConvKernelSize + } + } + + return kv +} + +func (p *gemma4Model) Tensors(ts []Tensor) []*ggml.Tensor { + // First pass: collect vision clamp scalar values into a packed tensor. + // Layout: per vision layer (0..N-1), 7 linears (q,k,v,out,gate,up,down) × 4 values (inMin,inMax,outMin,outMax). + // Then 4 values for the projector (mm.input_projection). + clampSuffixes := []string{".input_min", ".input_max", ".output_min", ".output_max"} + clampMap := make(map[string]float32) + for _, t := range ts { + name := t.Name() + for _, sfx := range clampSuffixes { + if strings.HasSuffix(name, sfx) && (strings.Contains(name, "vision_tower") || strings.Contains(name, "embed_vision")) { + var buf bytes.Buffer + t.WriteTo(&buf) + data := buf.Bytes() + if len(data) >= 4 { + clampMap[name] = math.Float32frombits(uint32(data[0]) | uint32(data[1])<<8 | uint32(data[2])<<16 | uint32(data[3])<<24) + } + } + } + } + + var out []*ggml.Tensor + for _, t := range ts { + name := t.Name() + + // Skip embedding_post_projection_norm — used as weightless RMS norm in inference + if strings.Contains(name, "embedding_post_projection_norm") { + continue + } + + // Vision tensor renaming: match published mmproj GGUF names + if strings.HasPrefix(name, "v.blk.") { + name = strings.Replace(name, ".attn_norm.", ".ln1.", 1) + name = strings.Replace(name, ".ffn_norm.", ".ln2.", 1) + name = strings.Replace(name, ".attn_output.", ".attn_out.", 1) + name = strings.Replace(name, ".post_attention_norm.", ".attn_post_norm.", 1) + name = strings.Replace(name, ".post_ffw_norm.", ".ffn_post_norm.", 1) + name = strings.Replace(name, ".layer_output_scale.", ".out_scale.", 1) + } + + // per_dim_scale: apply softplus to weight data and add .weight suffix. + if strings.HasPrefix(name, "a.blk.") && strings.HasSuffix(name, "per_dim_scale") { + name = name + ".weight" + t.SetRepacker(softplusRepacker) + } + + // Depthwise conv1d: squeeze middle dimension [C, 1, K] → [C, K]. + if strings.HasPrefix(name, "a.blk.") && strings.Contains(name, "conv_dw") && strings.HasSuffix(name, ".weight") { + t.SetRepacker(squeezeMiddleDim) + } + + shape := t.Shape() + + // Convert scalar tensors (input_min/max, output_min/max) to 1D + if len(shape) == 0 { + shape = []uint64{1} + } + + // Depthwise conv1d shape: safetensors [C, 1, K] → GGUF ne[K, C]. + // Shape array here maps to GGUF ne[] directly, but safetensors reader + // stores shape in PyTorch order [C, 1, K] which the GGUF writer inverts. + // Published GGUF has ne[0]=K, ne[1]=C → shape array must be [K, C]. + if strings.HasPrefix(name, "a.blk.") && strings.Contains(name, "conv_dw") && strings.HasSuffix(name, ".weight") && len(shape) == 3 { + shape = []uint64{shape[0], shape[2]} + } + + // MoE expert weights: no transpose needed. Safetensors stores [experts, out, in] + // which the framework reverses to GGUF ne=[in, out, experts], matching ggml_mul_mat_id. + // (transposeExperts was incorrectly swapping dims — removed) + + // Audio conv weights are forced to F32 via tensorBase.Kind() in reader.go + // (im2col doesn't support BF16). No kindOverride needed — the Kind() method + // controls both the GGUF header type AND the WriteTo data encoding path. + var kindOverride *uint32 + + // Vision patch embedding: reshape from [n_embd, ksize_sq_c] to [n_embd, 3, patch_size, patch_size] + // Must be stored as F16 (not BF16) because the Conv2D im2col kernel requires F16/F32. + if strings.Contains(name, "v.patch_embd.weight") && len(shape) == 2 { + nEmbd := shape[0] + patchSize := uint64(p.VisionModel.PatchSize) + if patchSize == 0 { + patchSize = 16 + } + numCh := uint64(p.VisionModel.NumChannels) + if numCh == 0 { + numCh = 3 + } + t.SetRepacker(p.reshapePatchEmbed) + shape = []uint64{nEmbd, numCh, patchSize, patchSize} + f16Kind := uint32(1) // tensorKindFP16 + kindOverride = &f16Kind + } + + // Vision position embedding: keep 3D [2, maxPos, nEmbd] — matching published mmproj format. + // The framework reverses shape to GGUF ne=[nEmbd, maxPos, 2]. No data repacking needed. + + kind := t.Kind() + if kindOverride != nil { + kind = *kindOverride + } + out = append(out, &ggml.Tensor{ + Name: name, + Kind: kind, + Shape: shape, + WriterTo: t, + }) + } + + // Generate a single global rope_freqs.weight for proportional RoPE on global attention layers. + // This matches the published GGUF format: one global tensor shared by all layers. + // Global layers use partial_rotary_factor (0.25) — only rotate that fraction of dims. + // Dimensions beyond the rotated portion get freq_factor=1e30 (effectively no rotation). + tc := p.TextModel + if tc.GlobalHeadDim > 0 { + globalFreqsSize := tc.GlobalHeadDim / 2 // freq_factors are per dimension pair + + // Compute number of rotated pairs for global layers + partialRotaryFactor := float32(0.25) // default + if rp, ok := tc.RopeParameters["full_attention"]; ok && rp != nil && rp.PartialRotaryFactor != nil { + partialRotaryFactor = *rp.PartialRotaryFactor + } + nRotFull := int(float32(tc.GlobalHeadDim) * partialRotaryFactor / 2) + + freqs := make(ropeFactor, globalFreqsSize) + for j := range freqs { + if j < nRotFull { + freqs[j] = 1.0 + } else { + freqs[j] = 1e30 // effectively disable rotation + } + } + out = append(out, &ggml.Tensor{ + Name: "rope_freqs.weight", + Kind: 0, // F32 + Shape: []uint64{uint64(len(freqs))}, + WriterTo: freqs, + }) + } + + // Emit packed vision clamp data as a single F32 tensor. + // Layout: numLayers × 7 linears (q,k,v,out,gate,up,down) × 4 floats (inMin,inMax,outMin,outMax) + // then 4 floats for the projector. Total = (numLayers*7 + 1) * 4 floats. + if len(clampMap) > 0 { + numLayers := int(p.VisionModel.NumHiddenLayers) + linearNames := []string{"attn_q", "attn_k", "attn_v", "attn_out", "ffn_gate", "ffn_up", "ffn_down"} + suffixes := []string{".input_min", ".input_max", ".output_min", ".output_max"} + + totalFloats := (numLayers*len(linearNames) + 1) * 4 // +1 for projector + clampData := make([]float32, totalFloats) + + for layer := range numLayers { + for li, ln := range linearNames { + for si, sfx := range suffixes { + sfxMap := map[string]string{"attn_q": "q_proj", "attn_k": "k_proj", "attn_v": "v_proj", "attn_out": "o_proj", "ffn_gate": "gate_proj", "ffn_up": "up_proj", "ffn_down": "down_proj"} + for origName, val := range clampMap { + if strings.Contains(origName, fmt.Sprintf("layers.%d.", layer)) && strings.HasSuffix(origName, sfx) && strings.Contains(origName, sfxMap[ln]) { + idx := (layer*len(linearNames)+li)*4 + si + clampData[idx] = val + break + } + } + } + } + } + // Projector clamp values + projIdx := numLayers * len(linearNames) * 4 + for si, sfx := range suffixes { + for origName, val := range clampMap { + if strings.Contains(origName, "input_projection") && strings.HasSuffix(origName, sfx) { + clampData[projIdx+si] = val + break + } + } + } + + var buf bytes.Buffer + binary.Write(&buf, binary.LittleEndian, clampData) + out = append(out, &ggml.Tensor{ + Name: "v.clamp_data", + Kind: 0, // F32 + Shape: []uint64{uint64(totalFloats)}, + WriterTo: &buf, + }) + } + + return out +} + +// reshapePatchEmbed reshapes the vision patch embedding from HF layout [n_embd, ksize*ksize*channels] +// to GGUF layout [n_embd, channels, patch_size, patch_size]. +func (*gemma4Model) reshapePatchEmbed(_ string, data []float32, shape []uint64) ([]float32, error) { + if len(shape) != 2 { + return data, nil + } + nEmbd := int(shape[0]) + ksqC := int(shape[1]) + nChannels := 3 + patchSize := int(math.Sqrt(float64(ksqC / nChannels))) + + // HF layout: [n_embd, patch_size * patch_size * channels] (row-major) + // Need: [n_embd, channels, patch_size, patch_size] + result := make([]float32, len(data)) + for e := range nEmbd { + for c := range nChannels { + for h := range patchSize { + for w := range patchSize { + srcIdx := e*ksqC + h*patchSize*nChannels + w*nChannels + c + dstIdx := e*nChannels*patchSize*patchSize + c*patchSize*patchSize + h*patchSize + w + result[dstIdx] = data[srcIdx] + } + } + } + } + shape[0] = uint64(nEmbd) + shape[1] = uint64(nChannels * patchSize * patchSize) + return result, nil +} + +// softplusRepacker applies softplus (ln(1 + exp(x))) to tensor data. +// Used for per_dim_scale tensors which the published GGUF stores pre-activated. +func softplusRepacker(_ string, data []float32, shape []uint64) ([]float32, error) { + result := make([]float32, len(data)) + for i, x := range data { + result[i] = float32(math.Log(1 + math.Exp(float64(x)))) + } + return result, nil +} + +// squeezeMiddleDim squeezes the middle dimension from [C, 1, K] → [C, K] for depthwise conv1d weights. +// Data layout stays the same since the middle dim is 1 — just a shape change. +func squeezeMiddleDim(_ string, data []float32, _ []uint64) ([]float32, error) { + return data, nil +} + +func (p *gemma4Model) Replacements() []string { + return []string{ + // ClippableLinear wraps nn.Linear — strip .linear. from weight path + ".linear.weight", ".weight", + ".linear.bias", ".bias", + + // Audio SSCP (Sub-Sample Convolution Projection) + "model.audio_tower.subsample_conv_projection.conv_0.conv", "a.conv1d.0", + "model.audio_tower.subsample_conv_projection.conv_0.norm", "a.conv1d.0.norm", + "model.audio_tower.subsample_conv_projection.conv_1.conv", "a.conv1d.1", + "model.audio_tower.subsample_conv_projection.conv_1.norm", "a.conv1d.1.norm", + "model.audio_tower.subsample_conv_projection.layer0.conv", "a.conv1d.0", + "model.audio_tower.subsample_conv_projection.layer0.norm", "a.conv1d.0.norm", + "model.audio_tower.subsample_conv_projection.layer1.conv", "a.conv1d.1", + "model.audio_tower.subsample_conv_projection.layer1.norm", "a.conv1d.1.norm", + "model.audio_tower.subsample_conv_projection.input_proj_linear", "a.pre_encode.out", + + // Audio conformer blocks + "model.audio_tower.conformer", "a.blk", + "model.audio_tower.layers", "a.blk", + + // Audio conformer attention + "attention.attn.relative_position_embedding.pos_proj", "linear_pos", + "self_attn.relative_k_proj", "linear_pos", + "attention.attn.per_dim_key_scale", "per_dim_k_scale", + "attention.attn.per_dim_scale", "per_dim_scale", + "self_attn.per_dim_scale", "per_dim_scale", + "attention.attn.q_proj", "attn_q", + "attention.attn.k_proj", "attn_k", + "attention.attn.v_proj", "attn_v", + "attention.pre_attn_norm", "ln1", + "attention.post_norm", "ln2", + "attention.post", "attn_out", + "self_attn.post", "attn_out", + "norm_pre_attn", "ln1", + "norm_post_attn", "ln2", + + // Audio conformer feedforward + "ffw_layer_start.pre_layer_norm", "ffn_norm", + "ffw_layer_start.post_layer_norm", "ffn_post_norm", + "ffw_layer_start.ffw_layer_1", "ffn_up", + "ffw_layer_start.ffw_layer_2", "ffn_down", + "ffw_layer_end.pre_layer_norm", "ffn_norm_1", + "ffw_layer_end.post_layer_norm", "ffn_post_norm_1", + "ffw_layer_end.ffw_layer_1", "ffn_up_1", + "ffw_layer_end.ffw_layer_2", "ffn_down_1", + "feed_forward1.pre_layer_norm", "ffn_norm", + "feed_forward1.post_layer_norm", "ffn_post_norm", + "feed_forward1.ffw_layer_1", "ffn_up", + "feed_forward1.ffw_layer_2", "ffn_down", + "feed_forward2.pre_layer_norm", "ffn_norm_1", + "feed_forward2.post_layer_norm", "ffn_post_norm_1", + "feed_forward2.ffw_layer_1", "ffn_up_1", + "feed_forward2.ffw_layer_2", "ffn_down_1", + + // Audio conformer lightweight conv1d + "lconv1d.depthwise_conv1d", "conv_dw", + "lconv1d.pre_layer_norm", "conv_norm", + "lconv1d.conv_norm", "norm_conv", + "lconv1d.linear_start", "conv_pw1", + "lconv1d.linear_end", "conv_pw2", + + // Audio block final norm + "norm_out", "layer_pre_norm", + + // Audio embedder and output projection + "model.embed_audio.embedding_projection", "mm.a.input_projection", + "model.audio_tower.output_proj", "mm.a.fc", + + // Vision encoder + "model.vision_tower.encoder.layers", "v.blk", + "model.vision_tower.patch_embedder.input_proj", "v.patch_embd", + "model.vision_tower.patch_embedder.position_embedding_table", "v.position_embd.weight", + "model.vision_tower.std_bias", "v.std_bias", + "model.vision_tower.std_scale", "v.std_scale", + + // Vision multimodal projector + "model.embed_vision.embedding_projection", "mm.input_projection", + + // Text model + "model.language_model.embed_tokens_per_layer", "per_layer_token_embd", + "model.language_model.embed_tokens", "token_embd", + "model.language_model.per_layer_model_projection", "per_layer_model_proj", + "model.language_model.per_layer_projection_norm", "per_layer_proj_norm", + "model.language_model.norm", "output_norm", + "model.language_model.layers", "blk", + + // Shared attention replacements (work for both text and vision tensors) + "input_layernorm", "attn_norm", + "self_attn.q_proj", "attn_q", + "self_attn.q_norm", "attn_q_norm", + "self_attn.k_proj", "attn_k", + "self_attn.k_norm", "attn_k_norm", + "self_attn.v_proj", "attn_v", + "self_attn.o_proj", "attn_output", + "mlp.gate_proj", "ffn_gate", + "mlp.down_proj", "ffn_down", + "mlp.up_proj", "ffn_up", + + // Post norms + "post_attention_layernorm", "post_attention_norm", + "pre_feedforward_layernorm_2", "pre_ffw_norm_2", + "pre_feedforward_layernorm", "ffn_norm", + "post_feedforward_layernorm_1", "post_ffw_norm_1", + "post_feedforward_layernorm_2", "post_ffw_norm_2", + "post_feedforward_layernorm", "post_ffw_norm", + + // PLE + "per_layer_input_gate", "inp_gate", + "per_layer_projection", "proj", + "post_per_layer_input_norm", "post_norm", + + // MoE + "router.proj", "ffn_gate_inp", + "router.scale", "ffn_gate_inp.scale", + "router.per_expert_scale.weight", "ffn_down_exps.scale", + "router.per_expert_scale", "ffn_down_exps.scale", + "experts.gate_up_proj.weight", "ffn_gate_up_exps.weight", + "experts.gate_up_proj", "ffn_gate_up_exps.weight", + "experts.down_proj.weight", "ffn_down_exps.weight", + "experts.down_proj", "ffn_down_exps.weight", + "moe.gate_proj", "ffn_gate_exps.weight", + "moe.up_proj", "ffn_up_exps.weight", + "moe.gate_up_proj.weight", "ffn_gate_up_exps.weight", + "moe.gate_up_proj", "ffn_gate_up_exps.weight", + "moe.down_proj", "ffn_down_exps.weight", + "moe.per_expert_scale.weight", "ffn_down_exps.scale", + "moe.per_expert_scale", "ffn_down_exps.scale", + + // Layer scalar + "layer_scalar", "layer_output_scale.weight", + } +} diff --git a/convert/convert_gemma4_test.go b/convert/convert_gemma4_test.go new file mode 100644 index 000000000..54712dffd --- /dev/null +++ b/convert/convert_gemma4_test.go @@ -0,0 +1,318 @@ +package convert + +import ( + "strings" + "testing" +) + +func TestGemma4AudioReplacements(t *testing.T) { + p := gemma4Model{} + r := strings.NewReplacer(p.Replacements()...) + + tests := []struct { + name string + in string + want string + }{ + // SSCP convolution blocks + { + "sscp conv0 weight", + "model.audio_tower.subsample_conv_projection.conv_0.conv.weight", + "a.conv1d.0.weight", + }, + { + "sscp conv0 norm", + "model.audio_tower.subsample_conv_projection.conv_0.norm.weight", + "a.conv1d.0.norm.weight", + }, + { + "sscp conv1 weight", + "model.audio_tower.subsample_conv_projection.conv_1.conv.weight", + "a.conv1d.1.weight", + }, + { + "sscp input proj weight", + "model.audio_tower.subsample_conv_projection.input_proj_linear.weight", + "a.pre_encode.out.weight", + }, + { + "sscp input proj bias", + "model.audio_tower.subsample_conv_projection.input_proj_linear.bias", + "a.pre_encode.out.bias", + }, + { + "sscp layer0 conv weight (new naming)", + "model.audio_tower.subsample_conv_projection.layer0.conv.weight", + "a.conv1d.0.weight", + }, + { + "sscp layer1 norm weight (new naming)", + "model.audio_tower.subsample_conv_projection.layer1.norm.weight", + "a.conv1d.1.norm.weight", + }, + + // Conformer attention + { + "attn q weight", + "model.audio_tower.conformer.0.attention.attn.q_proj.linear.weight", + "a.blk.0.attn_q.weight", + }, + { + "attn k weight", + "model.audio_tower.conformer.5.attention.attn.k_proj.linear.weight", + "a.blk.5.attn_k.weight", + }, + { + "attn v clamp input_min", + "model.audio_tower.conformer.0.attention.attn.v_proj.input_min", + "a.blk.0.attn_v.input_min", + }, + { + "attn out weight (ClippableLinear)", + "model.audio_tower.conformer.0.attention.post.linear.weight", + "a.blk.0.attn_out.weight", + }, + { + "attn out clamp output_max", + "model.audio_tower.conformer.0.attention.post.output_max", + "a.blk.0.attn_out.output_max", + }, + { + "attn pre norm", + "model.audio_tower.conformer.0.attention.pre_attn_norm.weight", + "a.blk.0.ln1.weight", + }, + { + "attn post norm", + "model.audio_tower.conformer.0.attention.post_norm.weight", + "a.blk.0.ln2.weight", + }, + { + "linear pos", + "model.audio_tower.conformer.0.attention.attn.relative_position_embedding.pos_proj.weight", + "a.blk.0.linear_pos.weight", + }, + { + "per dim scale", + "model.audio_tower.conformer.0.attention.attn.per_dim_scale", + "a.blk.0.per_dim_scale", + }, + { + "per dim key scale", + "model.audio_tower.conformer.0.attention.attn.per_dim_key_scale", + "a.blk.0.per_dim_k_scale", + }, + { + "attn relative k proj (new naming)", + "model.audio_tower.layers.0.self_attn.relative_k_proj.weight", + "a.blk.0.linear_pos.weight", + }, + { + "attn pre norm (new naming)", + "model.audio_tower.layers.0.norm_pre_attn.weight", + "a.blk.0.ln1.weight", + }, + { + "attn post norm (new naming)", + "model.audio_tower.layers.0.norm_post_attn.weight", + "a.blk.0.ln2.weight", + }, + { + "attn out clamp output_max (new naming)", + "model.audio_tower.layers.0.self_attn.post.output_max", + "a.blk.0.attn_out.output_max", + }, + { + "per dim scale (new naming)", + "model.audio_tower.layers.0.self_attn.per_dim_scale", + "a.blk.0.per_dim_scale", + }, + + // Conformer feedforward start + { + "ffn up weight", + "model.audio_tower.conformer.0.ffw_layer_start.ffw_layer_1.linear.weight", + "a.blk.0.ffn_up.weight", + }, + { + "ffn down weight", + "model.audio_tower.conformer.0.ffw_layer_start.ffw_layer_2.linear.weight", + "a.blk.0.ffn_down.weight", + }, + { + "ffn norm", + "model.audio_tower.conformer.0.ffw_layer_start.pre_layer_norm.weight", + "a.blk.0.ffn_norm.weight", + }, + { + "ffn post norm", + "model.audio_tower.conformer.0.ffw_layer_start.post_layer_norm.weight", + "a.blk.0.ffn_post_norm.weight", + }, + + // Conformer feedforward end + { + "ffn up 1 weight", + "model.audio_tower.conformer.0.ffw_layer_end.ffw_layer_1.linear.weight", + "a.blk.0.ffn_up_1.weight", + }, + { + "ffn down 1 weight", + "model.audio_tower.conformer.0.ffw_layer_end.ffw_layer_2.linear.weight", + "a.blk.0.ffn_down_1.weight", + }, + { + "ffn norm 1", + "model.audio_tower.conformer.0.ffw_layer_end.pre_layer_norm.weight", + "a.blk.0.ffn_norm_1.weight", + }, + { + "ffn post norm 1", + "model.audio_tower.conformer.0.ffw_layer_end.post_layer_norm.weight", + "a.blk.0.ffn_post_norm_1.weight", + }, + { + "ffn up output_max (new naming)", + "model.audio_tower.layers.10.feed_forward1.ffw_layer_1.output_max", + "a.blk.10.ffn_up.output_max", + }, + { + "ffn down output_min (new naming)", + "model.audio_tower.layers.0.feed_forward1.ffw_layer_2.output_min", + "a.blk.0.ffn_down.output_min", + }, + { + "ffn up 1 input_max (new naming)", + "model.audio_tower.layers.0.feed_forward2.ffw_layer_1.input_max", + "a.blk.0.ffn_up_1.input_max", + }, + { + "ffn norm 1 (new naming)", + "model.audio_tower.layers.0.feed_forward2.pre_layer_norm.weight", + "a.blk.0.ffn_norm_1.weight", + }, + + // Conformer lightweight conv1d + { + "conv dw weight", + "model.audio_tower.conformer.0.lconv1d.depthwise_conv1d.weight", + "a.blk.0.conv_dw.weight", + }, + { + "conv norm (pre_layer_norm)", + "model.audio_tower.conformer.0.lconv1d.pre_layer_norm.weight", + "a.blk.0.conv_norm.weight", + }, + { + "norm conv (conv_norm)", + "model.audio_tower.conformer.0.lconv1d.conv_norm.weight", + "a.blk.0.norm_conv.weight", + }, + { + "conv pw1 weight", + "model.audio_tower.conformer.0.lconv1d.linear_start.linear.weight", + "a.blk.0.conv_pw1.weight", + }, + { + "conv pw2 weight", + "model.audio_tower.conformer.0.lconv1d.linear_end.linear.weight", + "a.blk.0.conv_pw2.weight", + }, + + // Audio embedder + { + "audio embedder projection weight", + "model.embed_audio.embedding_projection.linear.weight", + "mm.a.input_projection.weight", + }, + { + "audio embedder projection bias", + "model.embed_audio.embedding_projection.linear.bias", + "mm.a.input_projection.bias", + }, + + // Audio output projection + { + "audio output proj weight", + "model.audio_tower.output_proj.weight", + "mm.a.fc.weight", + }, + { + "audio output proj bias", + "model.audio_tower.output_proj.bias", + "mm.a.fc.bias", + }, + + // Verify vision tensors still work + { + "vision q weight", + "model.vision_tower.encoder.layers.0.self_attn.q_proj.linear.weight", + "v.blk.0.attn_q.weight", + }, + { + "vision std bias", + "model.vision_tower.std_bias", + "v.std_bias", + }, + { + "vision std scale", + "model.vision_tower.std_scale", + "v.std_scale", + }, + { + "vision patch embd", + "model.vision_tower.patch_embedder.input_proj.weight", + "v.patch_embd.weight", + }, + { + "vision projector", + "model.embed_vision.embedding_projection.linear.weight", + "mm.input_projection.weight", + }, + + // Verify text tensors still work + { + "text attn q", + "model.language_model.layers.0.self_attn.q_proj.weight", + "blk.0.attn_q.weight", + }, + { + "text token embd", + "model.language_model.embed_tokens.weight", + "token_embd.weight", + }, + { + "text moe gate up fused", + "model.language_model.layers.0.experts.gate_up_proj", + "blk.0.ffn_gate_up_exps.weight", + }, + { + "text moe down", + "model.language_model.layers.0.experts.down_proj", + "blk.0.ffn_down_exps.weight", + }, + { + "text moe down with weight suffix", + "model.language_model.layers.0.experts.down_proj.weight", + "blk.0.ffn_down_exps.weight", + }, + { + "text moe per expert scale", + "model.language_model.layers.0.router.per_expert_scale", + "blk.0.ffn_down_exps.scale", + }, + { + "text moe per expert scale with weight suffix", + "model.language_model.layers.0.router.per_expert_scale.weight", + "blk.0.ffn_down_exps.scale", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := r.Replace(tt.in); got != tt.want { + t.Errorf("Replace(%q) = %q, want %q", tt.in, got, tt.want) + } + }) + } +} diff --git a/convert/convert_test.go b/convert/convert_test.go index fa5d7488a..45941f6bb 100644 --- a/convert/convert_test.go +++ b/convert/convert_test.go @@ -205,8 +205,8 @@ func TestConvertInvalidDatatype(t *testing.T) { generateSafetensorTestData(t, tempDir, td) err = ConvertModel(os.DirFS(tempDir), f) - if err == nil || err.Error() != "unsupported safetensors model" { - t.Errorf("expected error but didn't get one") + if err == nil || !strings.Contains(err.Error(), "unknown data type") { + t.Errorf("expected 'unknown data type' error but got: %v", err) } } diff --git a/convert/reader.go b/convert/reader.go index 0cff12a22..ec2da4a23 100644 --- a/convert/reader.go +++ b/convert/reader.go @@ -42,8 +42,11 @@ func (t tensorBase) Kind() uint32 { strings.HasSuffix(t.name, ".bias") || strings.HasSuffix(t.name, ".shortconv.conv.weight") || strings.HasSuffix(t.name, ".ssm_conv1d.weight") || // SSM conv kernel must be F32 for Metal + strings.HasPrefix(t.name, "a.conv1d.") || // audio SSCP conv weights must be F32 for im2col + strings.Contains(t.name, ".conv_dw.") || // audio depthwise conv weights must be F32 t.name == "token_types.weight" || t.name == "v.positional_embedding_vlm" || + t.name == "v.position_embd.weight" || t.name == "v.tile_position_embd.weight" || t.name == "v.pre_tile_position_embd.weight" || t.name == "v.post_tile_position_embd.weight" || diff --git a/convert/reader_safetensors.go b/convert/reader_safetensors.go index f7dae0646..6127ab566 100644 --- a/convert/reader_safetensors.go +++ b/convert/reader_safetensors.go @@ -5,7 +5,6 @@ import ( "bytes" "encoding/binary" "encoding/json" - "errors" "fmt" "io" "io/fs" @@ -53,9 +52,10 @@ func parseSafetensors(fsys fs.FS, replacer *strings.Replacer, ps ...string) ([]T for _, key := range keys { if value := headers[key]; value.Type != "" { - // bitsandbytes quantized models are unsupported + // Scalar tensors (e.g. clipped linear min/max) are 0-dim in safetensors. + // Promote them to 1-dim so they can be stored in GGUF. if len(value.Shape) == 0 { - return nil, errors.New("unsupported safetensors model") + value.Shape = []uint64{1} } ggufName := replacer.Replace(key) if _, ok := names[ggufName]; ok { diff --git a/fs/ggml/ggml.go b/fs/ggml/ggml.go index c835cb32b..9788297cc 100644 --- a/fs/ggml/ggml.go +++ b/fs/ggml/ggml.go @@ -281,6 +281,7 @@ func (kv KV) OllamaEngineRequired() bool { "deepseekocr", "gemma3", "gemma3n", + "gemma4", "gptoss", "gpt-oss", "llama4", "mistral3", diff --git a/integration/audio_test.go b/integration/audio_test.go new file mode 100644 index 000000000..6d169f1c5 --- /dev/null +++ b/integration/audio_test.go @@ -0,0 +1,259 @@ +//go:build integration + +package integration + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "mime/multipart" + "net/http" + "strings" + "testing" + "time" + + "github.com/ollama/ollama/api" +) + +var defaultAudioModels = []string{ + "gemma4:e2b", + "gemma4:e4b", +} + +// decodeTestAudio returns the test audio clip ("Why is the sky blue?", 16kHz mono WAV). +func decodeTestAudio(t *testing.T) api.ImageData { + t.Helper() + data, err := base64.StdEncoding.DecodeString(audioEncodingPrompt) + if err != nil { + t.Fatalf("failed to decode test audio: %v", err) + } + return data +} + +// setupAudioModel pulls the model, preloads it, and skips if it doesn't support audio. +func setupAudioModel(ctx context.Context, t *testing.T, client *api.Client, model string) { + t.Helper() + requireCapability(ctx, t, client, model, "audio") + pullOrSkip(ctx, t, client, model) + err := client.Generate(ctx, &api.GenerateRequest{Model: model}, func(response api.GenerateResponse) error { return nil }) + if err != nil { + t.Fatalf("failed to load model %s: %s", model, err) + } +} + +// TestAudioTranscription tests that the model can transcribe audio to text. +func TestAudioTranscription(t *testing.T) { + for _, model := range testModels(defaultAudioModels) { + t.Run(model, func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + defer cancel() + client, _, cleanup := InitServerConnection(ctx, t) + defer cleanup() + + setupAudioModel(ctx, t, client, model) + audio := decodeTestAudio(t) + noThink := &api.ThinkValue{Value: false} + + req := api.ChatRequest{ + Model: model, + Think: noThink, + Messages: []api.Message{ + { + Role: "system", + Content: "Transcribe the audio exactly as spoken. Output only the transcription.", + }, + { + Role: "user", + Content: "Transcribe this audio.", + Images: []api.ImageData{audio}, + }, + }, + Stream: &stream, + Options: map[string]any{ + "temperature": 0, + "seed": 123, + "num_predict": 50, + }, + } + + // The audio says "Why is the sky blue?" — expect key words in transcription. + DoChat(ctx, t, client, req, []string{"sky", "blue"}, 60*time.Second, 10*time.Second) + }) + } +} + +// TestAudioResponse tests that the model can respond to a spoken question. +func TestAudioResponse(t *testing.T) { + for _, model := range testModels(defaultAudioModels) { + t.Run(model, func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + defer cancel() + client, _, cleanup := InitServerConnection(ctx, t) + defer cleanup() + + setupAudioModel(ctx, t, client, model) + audio := decodeTestAudio(t) + noThink := &api.ThinkValue{Value: false} + + req := api.ChatRequest{ + Model: model, + Think: noThink, + Messages: []api.Message{ + { + Role: "user", + Content: "", + Images: []api.ImageData{audio}, + }, + }, + Stream: &stream, + Options: map[string]any{ + "temperature": 0, + "seed": 123, + "num_predict": 200, + }, + } + + // The audio asks "Why is the sky blue?" — expect an answer about light/scattering. + DoChat(ctx, t, client, req, []string{ + "scatter", "light", "blue", "atmosphere", "wavelength", "rayleigh", + }, 60*time.Second, 10*time.Second) + }) + } +} + +// TestOpenAIAudioTranscription tests the /v1/audio/transcriptions endpoint. +func TestOpenAIAudioTranscription(t *testing.T) { + for _, model := range testModels(defaultAudioModels) { + t.Run(model, func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + defer cancel() + client, endpoint, cleanup := InitServerConnection(ctx, t) + defer cleanup() + + setupAudioModel(ctx, t, client, model) + audioBytes := decodeTestAudio(t) + + // Build multipart form request. + var body bytes.Buffer + writer := multipart.NewWriter(&body) + writer.WriteField("model", model) + part, err := writer.CreateFormFile("file", "prompt.wav") + if err != nil { + t.Fatal(err) + } + part.Write(audioBytes) + writer.Close() + + url := fmt.Sprintf("http://%s/v1/audio/transcriptions", endpoint) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, &body) + if err != nil { + t.Fatal(err) + } + req.Header.Set("Content-Type", writer.FormDataContentType()) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + t.Fatalf("request failed: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + respBody, _ := io.ReadAll(resp.Body) + t.Fatalf("expected 200, got %d: %s", resp.StatusCode, string(respBody)) + } + + respBody, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatal(err) + } + + text := strings.ToLower(string(respBody)) + if !strings.Contains(text, "sky") && !strings.Contains(text, "blue") { + t.Errorf("transcription response missing expected words, got: %s", string(respBody)) + } + }) + } +} + +// TestOpenAIChatWithAudio tests /v1/chat/completions with input_audio content. +func TestOpenAIChatWithAudio(t *testing.T) { + for _, model := range testModels(defaultAudioModels) { + t.Run(model, func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + defer cancel() + client, endpoint, cleanup := InitServerConnection(ctx, t) + defer cleanup() + + setupAudioModel(ctx, t, client, model) + audioB64 := audioEncodingPrompt + + reqBody := fmt.Sprintf(`{ + "model": %q, + "messages": [{ + "role": "user", + "content": [ + {"type": "input_audio", "input_audio": {"data": %q, "format": "wav"}} + ] + }], + "temperature": 0, + "seed": 123, + "max_tokens": 200, + "think": false + }`, model, strings.TrimSpace(audioB64)) + + url := fmt.Sprintf("http://%s/v1/chat/completions", endpoint) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, strings.NewReader(reqBody)) + if err != nil { + t.Fatal(err) + } + req.Header.Set("Content-Type", "application/json") + + resp, err := http.DefaultClient.Do(req) + if err != nil { + t.Fatalf("request failed: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + respBody, _ := io.ReadAll(resp.Body) + t.Fatalf("expected 200, got %d: %s", resp.StatusCode, string(respBody)) + } + + respBytes, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatalf("failed to read response: %v", err) + } + + var result struct { + Choices []struct { + Message struct { + Content string `json:"content"` + Reasoning string `json:"reasoning"` + } `json:"message"` + } `json:"choices"` + } + if err := json.Unmarshal(respBytes, &result); err != nil { + t.Fatalf("failed to decode response: %v", err) + } + + if len(result.Choices) == 0 { + t.Fatal("no choices in response") + } + + text := strings.ToLower(result.Choices[0].Message.Content + " " + result.Choices[0].Message.Reasoning) + found := false + for _, word := range []string{"sky", "blue", "scatter", "light", "atmosphere"} { + if strings.Contains(text, word) { + found = true + break + } + } + if !found { + t.Errorf("response missing expected words about sky/blue/light, got: %s", result.Choices[0].Message.Content) + } + }) + } +} diff --git a/integration/audio_test_data_test.go b/integration/audio_test_data_test.go new file mode 100644 index 000000000..b3ad8bc9e --- /dev/null +++ b/integration/audio_test_data_test.go @@ -0,0 +1,9 @@ +//go:build integration + +package integration + +// audioEncodingPrompt is a 16kHz mono WAV recording of "Why is the sky blue?" +// (~5.3 seconds, 168KB). Used for audio transcription and response integration tests. +const audioEncodingPrompt = ` +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/JtH//5vP//8Yzv//nMz//ynL//+9yf//Wcj///3G//+pxf//XMT//xfD///awf//pMD//3a///9Qvv//Mr3//xu8//8Mu///BLr//wW5//8MuP//G7f//zK2//9Qtf//drT//6Oz///Xsv//E7L//1Wx//+gsP//8a///0qv//+prv//EK7//36t///yrP//bqz///Cr//96q///Cqv//6Gq//8+qv//4an//4yp//89qf//9Kj//7Ko//92qP//QKj//xCo///np///w6f//6an//+Pp///faf//3Gn//9rp///a6f//3Cn//97p///i6f//6Cn//+7p///26f//wCo//8rqP//Wqj//4+o///IqP//Bqn//0mp//+Qqf//3Kn//y2q//+Cqv//3Kr//zqr//+cq///Aqz//2ys///arP//TK3//8Kt//88rv//uq7//zuv///Ar///SLD//9Ow//9isf//9LH//4qy//8is///vbP//1y0///9tP//obX//0i2///ytv//nrf//0y4///9uP//sbn//2a6//8eu///2Lv//5S8//9Tvf//E77//9S+//+Yv///XsD//yXB///twf//t8L//4PD//9QxP//HsX//+7F//+/xv//kcf//2PI//83yf//DMr//+LK//+4y///j8z//2fN//9Azv//Gc////LP///M0P//ptH//4HS//9c0///N9T//xLV///t1f//ydb//6TX//9/2P//Wtn//zXa//8Q2///69v//8Xc//+e3f//eN7//1Df//8p4P//AOH//9fh//+u4v//g+P//1jk//8s5f//AOb//9Lm//+j5///dOj//0Pp//8S6v//3+r//6vr//927P//QO3//wnu///Q7v//lu///1vw//8e8f//4PH//6Hy//9g8///HvT//9r0//+U9f//Tfb//wT3//+69///bvj//yH5///R+f//gPr//y77///Z+///g/z//yv9///Q/f//df7//xf///+4////VQAAAPIAAACMAQAAJQIAALwCAABRAwAA5AMAAHQEAAADBQAAkAUAABsGAACkBgAAKgcAAK8HAAAxCAAAsggAADAJAACtCQAAJwoAAJ8KAAAVCwAAiQsAAPsLAABqDAAA2AwAAEMNAACtDQAAFA4AAHkOAADcDgAAPQ8AAJwPAAD4DwAAUxAAAKsQAAACEQAAVhEAAKgRAAD4EQAARhIAAJESAADbEgAAIxMAAGgTAACsEwAA7RMAAC0UAABqFAAAphQAAN8UAAAWFQAATBUAAH8VAACxFQAA4BUAAA4WAAA5FgAAYxYAAIsWAACxFgAA1RYAAPcWAAAXFwAANRcAAFIXAABtFwAAhRcAAJ0XAACyFwAAxhcAANgXAADoFwAA9hcAAAMYAAAOGAAAGBgAAB8YAAAmGAAAKhgAAC0YAAAvGAAALxgAAC0YAAAqGAAAJRgAAB8YAAAYGAAADxgAAAQYAAD5FwAA6xcAAN0XAADNFwAAvBcAAKoXAACWFwAAgRcAAGsXAABTFwAAOxcAACEXAAAGFwAA6hYAAM0WAACuFgAAjxYAAG8WAABNFgAAKxYAAAgWAADjFQAAvhUAAJgVAABxFQAASBUAACAVAAD2FAAAyxQAAKAUAAB0FAAARxQAABkUAADrEwAAvBMAAIwTAABbEwAAKhMAAPkSAADGEgAAkxIAAGASAAAsEgAA9xEAAMIRAACNEQAAVxEAACARAADpEAAAshAAAHoQAABCEAAAChAAANEPAACYDwAAXw8AACUPAADrDgAAsQ4AAHYOAAA8DgAAAQ4AAMYNAACLDQAATw0AABQNAADYDAAAnQwAAGEMAAAlDAAA6QsAAK0LAABxCwAANQsAAPkKAAC9CgAAggoAAEYKAAAKCgAAzgkAAJMJAABXCQAAHAkAAOEIAACmCAAAawgAADAIAAD1BwAAuwcAAIEHAABHBwAADQcAANQGAACbBgAAYgYAACkGAADxBQAAuQUAAIEFAABKBQAAEgUAANwEAAClBAAAbwQAADoEAAAEBAAA0AMAAJsDAABnAwAAMwMAAAADAADNAgAAmwIAAGkCAAA3AgAABgIAANUBAAClAQAAdQEAAEYBAAAXAQAA6QAAALsAAACOAAAAYQAAADUAAAAKAAAA3////7T///+K////Yf///zj///8P////5/7//8D+//+Z/v//cv7//03+//8n/v//A/7//979//+7/f//mP3//3b9//9U/f//Mv3//xL9///y/P//0vz//7P8//+V/P//d/z//1r8//89/P//Ifz//wb8///r+///0Pv//7f7//+d+///hfv//237//9V+///P/v//yj7//8T+////fr//+n6///V+v//wfr//676//+c+v//ivr//3n6//9o+v//WPr//0n6//86+v//K/r//x36//8Q+v//A/r///f5///r+f//4Pn//9X5///L+f//wfn//7j5//+v+f//p/n//5/5//+Y+f//kfn//4v5//+F+f//gPn//3v5//93+f//c/n//3D5//9t+f//avn//2j5//9m+f//Zfn//2T5//9k+f//ZPn//2X5//9m+f//Z/n//2n5//9r+f//bfn//3D5//90+f//d/n//3v5//+A+f//hPn//4r5//+P+f//lfn//5v5//+h+f//qPn//6/5//+3+f//vvn//8b5///P+f//1/n//+D5///p+f//8/n///35//8H+v//Efr//xv6//8m+v//Mfr//zz6//9I+v//U/r//1/6//9r+v//ePr//4T6//+R+v//nvr//6v6//+4+v//xvr//9P6///h+v//7/r///36//8L+///Gvv//yj7//83+///Rvv//1T7//9j+///c/v//4L7//+R+///ofv//7D7///A+///z/v//9/7///v+/////v//w/8//8f/P//L/z//z/8//9P/P//X/z//3D8//+A/P//kPz//6H8//+x/P//wfz//9L8///i/P//8vz//wP9//8T/f//I/3//zT9//9E/f//VP3//2X9//91/f//hf3//5X9//+l/f//tv3//8b9///W/f//5v3///X9//8F/v//Ff7//yX+//80/v//RP7//1P+//9j/v//cv7//4H+//+Q/v//n/7//67+//+9/v//zP7//9v+///p/v//+P7//wb///8U////Iv///zD///8+////TP///1r///9n////dP///4L///+P////nP///6n///+2////wv///8/////b////5/////P/////////CgAAABYAAAAhAAAALAAAADgAAABDAAAATgAAAFgAAABjAAAAbQAAAHgAAACCAAAAjAAAAJYAAACfAAAAqQAAALIAAAC8AAAAxQAAAM4AAADWAAAA3wAAAOcAAADwAAAA+AAAAAABAAAIAQAADwEAABcBAAAeAQAAJQEAACwBAAAzAQAAOgEAAEABAABHAQAATQEAAFMBAABZAQAAXwEAAGQBAABqAQAAbwEAAHQBAAB5AQAAfgEAAIMBAACHAQAAjAEAAJABAACUAQAAmAEAAJwBAACfAQAAowEAAKYBAACpAQAArAEAAK8BAACyAQAAtQEAALcBAAC6AQAAvAEAAL4BAADAAQAAwgEAAMMBAADFAQAAxgEAAMgBAADJAQAAygEAAMsBAADMAQAAzAEAAM0BAADNAQAAzQEAAM4BAADOAQAAzgEAAM4BAADNAQAAzQEAAMwBAADMAQAAywEAAMoBAADJAQAAyAEAAMcBAADGAQAAxQEAAMMBAADCAQAAwAEAAL8BAAC9AQAAuwEAALkBAAC3AQAAtQEAALMBAACwAQAArgEAAKsBAACpAQAApgEAAKQBAAChAQAAngEAAJsBAACYAQAAlQEAAJIBAACPAQAAjAEAAIkBAACFAQAAggEAAH8BAAB7AQAAeAEAAHQBAABwAQAAbQEAAGkBAABlAQAAYQEAAF0BAABaAQAAVgEAAFIBAABOAQAASgEAAEYBAABBAQAAPQEAADkBAAA1AQAAMQEAACwBAAAoAQAAJAEAACABAAAbAQAAFwEAABMBAAAOAQAACgEAAAUBAAABAQAA/QAAAPgAAAD0AAAA7wAAAOsAAADmAAAA4gAAAN0AAADZAAAA1AAAANAAAADLAAAAxwAAAMMAAAC+AAAAugAAALUAAACxAAAArAAAAKgAAACjAAAAnwAAAJsAAACWAAAAkgAAAI4AAACJAAAAhQAAAIEAAAB9AAAAeAAAAHQAAABwAAAAbAAAAGgAAABjAAAAXwAAAFsAAABXAAAAUwAAAE8AAABLAAAARwAAAEMAAAA/AAAAPAAAADgAAAA0AAAAMAAAACwAAAApAAAAJQAAACIAAAAeAAAAGgAAABcAAAATAAAAEAAAAA0AAAAJAAAABgAAAAMAAAAAAAAA/f////r////3////9P////H////u////6////+j////l////4v///9/////c////2v///9f////U////0v///8/////N////yv///8j////F////w////8H///++////vP///7r///+4////tv///7T///+y////sP///67///+s////qv///6j///+n////pf///6P///+i////oP///5////+d////nP///5r///+Z////mP///5b///+V////lP///5P///+S////kf///5D///+P////jv///43///+M////i////4r///+K////if///4j///+I////h////4b///+G////hf///4X///+F////hP///4T///+D////g////4P///+D////g////4L///+C////gv///4L///+C////gv///4L///+C////gv///4P///+D////g////4P///+D////hP///4T///+E////hf///4X///+G////hv///4b///+H////h////4j///+J////if///4r///+K////i////4z///+M////jf///47///+P////j////5D///+R////kv///5P///+T////lP///5X///+W////l////5j///+Z////mv///5v///+c////nf///57///+f////oP///6H///+i////o////6T///+l////pv///6j///+p////qv///6v///+s////rf///67///+w////sf///7L///+z////tP///7X///+3////uP///7n///+6////vP///73///++////v////8D////C////w////8T////F////xv///8j////J////yv///8v////N////zv///8/////Q////0f///9P////U////1f///9b////X////2f///9r////b////3P///93////e////4P///+H////i////4////+T////l////5v///+j////p////6v///+v////s////7f///+7////v////8P////H////y////8/////T////1////9v////f////4////+f////r////7/////P////3////+/////////wAAAAAAAAAAAQAAAAEAAAACAAAAAwAAAAQAAAAFAAAABgAAAAYAAAAHAAAACAAAAAkAAAAJAAAACgAAAAsAAAAMAAAADAAAAA0AAAAOAAAADgAAAA8AAAAQAAAAEAAAABEAAAASAAAAEgAAABMAAAATAAAAFAAAABQAAAAVAAAAFgAAABYAAAAXAAAAFwAAABgAAAAYAAAAGAAAABkAAAAZAAAAGgAAABoAAAAbAAAAGwAAABsAAAAcAAAAHAAAABwAAAAdAAAAHQAAAB0AAAAeAAAAHgAAAB4AAAAfAAAAHwAAAB8AAAAfAAAAIAAAACAAAAAgAAAAIAAAACAAAAAhAAAAIQAAACEAAAAhAAAAIQAAACEAAAAhAAAAIQAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACIAAAAiAAAAIgAAACEAAAAhAAAAIQAAACEAAAAhAAAAIQAAACEAAAAhAAAAIQAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAAB8AAAAfAAAAHwAAAB8AAAAfAAAAHgAAAB4AAAAeAAAAHgAAAB4AAAAdAAAAHQAAAB0AAAAdAAAAHAAAABwAAAAcAAAAHAAAABsAAAAbAAAAGwAAABoAAAAaAAAAGgAAABoAAAAZAAAAGQAAABkAAAAYAAAAGAAAABgAAAAXAAAAFwAAABcAAAAXAAAAFgAAABYAAAAWAAAAFQAAABUAAAAVAAAAFAAAABQAAAAUAAAAEwAAABMAAAATAAAAEgAAABIAAAASAAAAEQAAABEAAAARAAAAEAAAABAAAAAQAAAADwAAAA8AAAAPAAAADgAAAA4AAAAOAAAADQAAAA0AAAANAAAADAAAAAwAAAAMAAAACwAAAAsAAAALAAAACgAAAAoAAAAKAAAACQAAAAkAAAAJAAAACAAAAAgAAAAIAAAACAAAAAcAAAAHAAAABwAAAAYAAAAGAAAABgAAAAUAAAAFAAAABQAAAAUAAAAEAAAABAAAAAQAAAADAAAAAwAAAAMAAAADAAAAAgAAAAIAAAACAAAAAQAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/////////////////////+/////v////7////+/////v////3////9/////f////3////9/////P////z////8/////P////z////8////+/////v////7////+/////v////7////+v////r////6////+v////r////6////+v////r////5////+f////n////5////+f////n////5////+f////n////4////+P////j////4////+P////j////4////+P////j////4////+P////j////4////+P////j////3////9/////f////3////9/////f////3////9/////f////3////9/////f////3////9/////f////3////9/////f////3////9/////f////3////9/////f////3////9/////f////3////9/////f////3////9/////f////3////9/////f////3////9/////f////3////9/////f////3////9/////f////3////9/////f////3////9/////j////4////+P////j////4////+f////n////5////+f////n////5////+f////r////6////+v////r////6////+v////r////6////+v////r////6////+v////r////7////+/////v////7////+/////v////7////+/////v////7////+/////v////7/////P////z////8/////P////z////8/////P////z////8/////P////z////8/////f////3////9/////f////3////9/////f////3////9/////f////3////9/////v////7////+/////v////7////+/////v////7////+/////v////7////+/////v//////////////////////////////////////////////////////////////////////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAACAAAAAgAAAAIAAAADAAAAAwAAAAMAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAADAAAAAwAAAAMAAAADAAAAAwAAAAIAAAACAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAAAAAD+////+P////H////o////3f///9L////G////uf///6v///+d////kP///4L///91////Z////1v///9O////Qv///zf///8s////If///xf///8N////BP////z+///0/v//7v7//+n+///l/v//4/7//+T+///p/v//8f7////+//8T////Lv///1L///9/////uP////3///9PAAAAsgAAACMBAACqAQAAPwIAAPICAADFAwAAcQQAAO0EAABEBQAAeQUAAJEFAACRBQAAfAUAAFYFAAAiBQAA4wQAAJoEAABKBAAA9AMAAJkDAAA7AwAA2gIAAHgCAAAUAgAAsAEAAEsBAADlAAAAgAAAAB0AAAC4////Vf////T+//+U/v//N/7//9z9//+G/f//Mf3//978//+N/P//Pvz///j7//+3+///dvv//zz7//8D+///1vr//6P6//+C+v//Yfr//0L6//8s+v//Gfr//wn6//8B+v//9fn//+35///r+f//6/n///D5///v+f///vn//wX6//8T+v//Fvr//y/6//9I+v//cfr//476///C+v//zfr//wf7//8i+///MPv//3T7///C+///9Pv//zH8//97/P//rPz//9P8///f/P//f/3//5P9///g/f//MP7//xX+///p/v//Hf///xf///+N////Vv///5r///8e////jv///wsAAABi////XQAAAFcBAACQAQAAjgIAANMBAABSAgAAyAIAADsDAAClAwAAyQIAAKQDAAAwAwAAYwMAAC4CAAC8AQAA3wIAAGUCAAAcAwAAGAUAALUDAACwBAAAfQMAAOsCAAAfBAAA+AQAAHEIAABdBwAA/wYAABgJAAD/BQAAzQMAAFsLAAA5CAAAGwYAACUJAACZBAAAagoAALMHAACSAwAA8AQAAKMFAADpBQAAxQQAAEoEAACyBQAAfAsAAB8MAACPDgAAUwoAADgNAADsDQAAVA0AAGwMAAAlEQAACQoAAEYKAADhDQAAcAoAAOYKAAAkCAAAjhAAAKEKAAC0DQAAugsAAKINAAAI/P//twEAAGEVAAAJEwAAUg8AACgKAACkHAAAvRkAACkXAABhGgAACREAALYIAABYCgAALA8AAKEKAAADCQAARg4AABACAACY/f//8P///zr4//8t+f//xv3//8r7//9lAAAAgQsAAPwPAACDEgAAOQIAABj///8y9f//cfH//xji//9BBAAAogUAAE30//8C9P//0eT//2Lk//8guf//QOv//6Ps//+O9v//6fn//0Li//+l2f//+db//xbs//9g1v//xef//3bw///Z4///D8v//+az///+xf//T9f//7Tf//8Otf//ppv///nV////4P//lOj//+rY///0wv//7Mr///zI///U2///guT//4Do//8i6///cNz//wTt//8xAwAAwwsAAIcpAAB/+v//u+7//8AuAABOIQAAAzoAAIVVAACOQQAABEkAAKdxAADjWQAAm14AACIcAAC1DwAAqCwAALoaAADyHQAAav///z8YAADY5v//UA0AAMXo//9U1P//5goAAJ4QAADYUAAABV8AAPBHAADxQwAAAB8AAHJkAACSSAAAHTgAAFAkAAD19f//PyUAADIfAABNYAAAXFAAAGkiAABCNgAAkY0AALlGAAC7TQAAwh0AAAUPAACgSgAAI1MAAAqxAADwSwAAeXIAABvCAADPiQAADr8AAPKEAABzdAAAkGgAADoYAACBXQAACFMAAElpAACFQAAAWjIAAITh//93vf//+O7//14BAADxCgAAIQAAAMP4//8btP//ajkAAPx7//94lP//58r//3GO//8TLwAAywYAAJu2///6q///men//xEoAADmRwAA4RMAANFwAACM6///M+///30HAACwCAAAiAUAADvc///3kf//Y7H//4Xt///xZQAAAMAAAE7x//9sMQAArfr//7YSAABes///fsP//4X0///kPwAA5O///19Y//8tof//nnH//yFT//9cNP//A+v+//dW//8pa///4bj//8C+///hIv//0/z+/wrA/v8mNv//Hbf+/8lR///9Uf//48z+/wyx/v972v7/qi3//wTU/v8wA///AUL//+ku//+OBf//tr/+/3Jb//+Gsv//bGz+/zy3/v9XSP//IRL//0PT//+4GP//QRP//3xmAADbJv///rb//6LB//9RGP//1Wv//yqp/v/vyv7/62n+/22b/v+jvP//hHf//58T///96/7/JSj//7R3///x1///1TsAAMzC///BKf//EBb//1UkAAA5XQAAxRIAAO9w//8JlAAAI5gBAEKLAACAGQIA+hUCAJAiAQAkSwEAHfcAAKG3AQBxOgIAIE4CAHq/AADmqQEAa8MBABfEAACZrgEAXpYBAJ5QAgAF0QIA2BkCAPCIAgBplQIAORgCADjlAgDpfQEAlswBAHNYAQA+0///s4cAAF25//8nSv//4IIBAKR9AAAk+QAAkmUBAOmdAADMwwAAhWkAAF0TAQCklf//zrUAAO/mAABtFwAAcdf//+2P/v9opf//8Fv//xtD//9cLwAA6jQAAIwRAACsZgAA0q0AAFuwAACoWQEAABj//z1lAADeHf//oeX9/9x3/f/Wjvz/ZwT9/3EG/f9B6vz/J/L7//g3/P883P3/tHz+/wnK/f/zBP//IYL9/9Qq//9HKf7/N/T+/0dIAABopP//4AkAAMysAACTSwAA3OX+/1aWAAArCQEAbI0BAMnbAQDCywAA+KgAAIDgAADY/P//Ef4BAOjG//+QQf3/p/b//8mh//9cKP7/9FH+/921/v+vJf//O0D//6oi//+MZP//ZWgAAF3s/v+8Pf7/Unj+/9KF/P8r7v3/zNn+/1pD/v9mhf7/o0X9/4/u+/+XZ/v/Y6z7/9fj+//TBf7/C3X+/40Y/f/Rhvz/JvH9/188/P+KSPv/IR37/0gX/P/S4P3/Kj/8/5Na/f+v1P3/dgUAAKhU/P826P3/5P39/3vF/f/tJAAAN1b+/x7I/f8vy/7/tLsAAJAQAQD6ZgEAueQAADnYBAAw7wAA+6T+/ypMAQBoOwIAMoT//yeOAQDcvQMAIXkDAEA3AwBVxwQA9F8HAOhCAwCk3wQAoDQGAJyBCACAAgkAuesGABeGBgA3iQYAC3AIAFSICQBmJgcAdWAJAF88BwBHpAgAIGEJAF9IBwC0RggASrMFAI0sBgCgOgQATJMEAE67BACsOQYALgoFACK6AwCRggMAkS0CAI98AgBgIgEAlRoBAH/QAgAGbgAAxqcAAMSK/P9G2fz/owD+/2u9+/9dQP7/uJ7//7HH//+GaP//a+P//+3FAABJ4wQAkNv+/x4vBAAM6QEADun7/zdwAgAC3fz/CIj9/+hB//8H/Pv/rIn+/zK6//+V2wUAAG4EACJcAgCQMgYANWwHAI4WDACb6gYAhWUJAN1HCACNHwQAv5oCAAu5///OYQIAjGQCAFs+/v/0M/r/WrL4/8sO+/+InPr/XYj5/3oy/f9EvPr/gH39/1ZB/P+Rvv3/EGT//333AQCjrQEAvMb7/3aF/f+6iPv/Omr+/3A4+f9zLvn/4zH7/x2h+f+fdfz/Kh/2/60l/P94Zvf/muz2/ziU+/+rG/n/cgf+/7uu9v/iB/v/V+77/5Oy9v8ZiPn/X/L//1TP+/8JJwIAibAAALUCAQCPkgMAjYf5/9C6BQAAsP//UVUHAKEYAQC5Cfz/OyIAAJu09//jnP//M0D+/7ZEAAClqvz/UKUCAO+F/v954P7/83kDABnyAgCL+QUACeH8/9vFBAAutAUAXwEGAB25BwBpJAUAmsACABkyBwAMXQIAsd0CAOTzAgCQafz/eR4DAARNBACaNgoA72EEALjNAwBepQUA+2kFAOgZDADClg0AbYQLABGaCADVAQkAqccHAD+pCgAfkQkAtI0BAGOUAABWQP7/EZP+/86wAwB1+gEA4WoBANyW/f8DCfz/J9/9/1Ww9v/Qxfr/EL3z/xKB8f/6DfL/LWns/09J6//wH+X/9wXs/2/75/+5jez/MTTp/16d7f8CrfP/Ylfv/3Wu9P+/Ier/KOnq/85S7f/EifL/Mufv/9zI6v8ilOb/6Lnq/18X6P95pOf/OJnu/9l18f8VFPb/0PXq/2a88v83G+3/AUzz/+43/P9w2///2Aj//zs5/v+pgQQAcV/+/+a+AQDtVwAAvRoHAAz3CwATpwYA7cgBAN79/v+Z0wEAcGgEAMmeAwAACwMAnMb///upAwBqgvv/bUEFAAaLAgA0I/v/uq4JAF0hBADswwkAbXAHAAZeAgBbVQkATm0HAM4EEQBW3BUAgOoUAJujGQA5BQsAKFwRAIXJEwCm+xEARnoTAFUbDgC89Q8AIcQQAOGbEgB+xBQA3SISAFdUEAAOmRsAV2MZAEl8FgBsQRkAcQAVALjiFwBh2hQASd4PADvRDQD/wA0A0/ATAAWbDADcAhUAPZoZAO7VFQCscRgA6KMOADOuEQB2YhQAhjkPACD/FAAhbQwAf14LAI3WEwDhgAgAg30KAMMzEAAQgxAAjd0OAKZxEAC5DxEAFc8SAMM7DgAlRgQACZr9//RR9f9eDPf/mHn7/8aE9f8PLvT/HWH3/2Ww8P+6dvb/znH5//IKAwBSOvr/ha3z/1fJ+v/t+Pb/gt78/yZo9f+J6fn/4ib8/xO8+f9fK/f/4cfx/zjJ+f+Z6///wqf7/7PT8/+X7Pb/lmv2/wgR9P+oIfL/Ojbz/6Zy7f+DMe7/yv7s/5zw4v9z4Oj/dhnt/6Be9P9KEO7/Vnnp/4/G8/+9guz/I5Hk/6085/8bqeH/ocLo/6N94P8T69//J2fj/6YJ2v+3Y+D/pxLh/2X/3f+QmOP/sPTl/5tQ4/84S+f/Dj/k/2FW5P9zMuH/VRTv/zGu6v9VFOT/123o/2Vb4v89gen/Whnk/2X25//uQvH/0T/w/7/j9P/pSPT/mnP+/4BHAAAfj////MsJAJY4CQDafRAAcJYGAOtfBACPChEAXGoRAHCzFQDXHhAA3AQOACjvFADfrBUAL4UVACJPEgBYQhcAvhAUAGCZDwAh+xUAyMoUAFcdHwDFxCMAbjEYAMXAEQBT6w4A1oUdAPJKHABPOhAAKMMXABtfEwAd3BIAJxkSAAdzFAAZ6hsA950SAEqPFQDHmRIAGPIUAKqPGACEMhEAsrUWANytCQAb9hAAt9IOAFbrCgCsDhEAXNwEACbtEgAq4QoAwDkOALCsEACF5AwAf6YTABnxCgDdIRIA+ooMANa5EACNShIAJAsLAD+lAwAYTAAANy8DAB6M+/8WCPn/bI/9/4zI/f+90/z/AAr9/y2I+v/G2PT/wXX6/1RX/P8/ZvL/00H5/6eH8v9Vh/P/Doj0//IE8v/0TPb/IiD2/0LO8P9kzvD/3/fw/880/P9jjPv/XGv6/3cQ/v9QwfD/6oL3/yhU9/8eb/X/FcD0/yxj7f+e0+z/Aori/1Sd4v+22e3/XRHh/3z88f840fD/o2v1/5to9f8LmOv/fL7y/9c28f+7B/j/ROzt/90t9P/+2fb/DnXu/8Yt9P86wfH/kvj5/8X2//8aSPz/wyQGAMyN9P92nPv/6D32/xXr7/8hmPf/W5Xy//aWBwAxIPL/BxD1/y4r+P+R1uz/NFf3/wyU8//fjP3/yJ36/7WSAwBoLQIAuxcCAMdKBQBYkv3/t+H2/0J4+f9kpgQAhoAAADOz/P+Wavv/Rzb0//Ua8v+jgvj/ohr//8J0BQDm0f3/LDMGANvsBAAnzQAAULcHAFGYEQCaiQ0AnjYKABjyFADfAxEAZnYIAAy+BgD/6QAAo+L9/4sA//+zEv//p8gBANFjAgBXpwMAh8wAADhL/P8JRP//bxYBABaZ/f940/7/9VcCAOVeBQD7nwwA+AkRAOKRDwADLREAIXYFAP+6EwAzVBoALHoRAJz4DADi3wsAC1sMAPO7DADEMhEAnqoQAEuoFgB7uhEAH/oTAEJhFgBNYxUAgGYTAOM2DwBiLw0AGbQQAIjxEwCdgg0A3g4LAJmQDgB5IBIAjAAOAOo2EgCyQhMAZdkWAG1UDQDzXf//nSEIAFbd+//YRf3/0wr//5f9+v/uZvX/Vhj7/7dQAACALfz/RJQGALBqBwCw+goASn///43h+/8vBAUAAPv//73tAABPS/v/cnP4/zSqAwCxa/f/D9QCAHmdAQAt9v3/D7ELADioAQA0/gQAgnb//31RBACh8fr/WcH7/+2o+/++7vf/W3UFAIcMBQDIjwgA0BsEAC5e+v+lDv//pK3+/2d0+f+yn/3/5pj6/yIhAgA5bfj/QDP5/1P/9P8kne3/vgTx/xuQ+v9xkv3/pA35/3EL+v8WA/7/l0wPADCvEACCGA0AbLIRAFbmEgAP/QYA3t0OAJZ9CgBslAYAQ7MCAMjKAAD6RQUAprEFAGloEACzZhAAW08LAC/TDACrqgsAOb0GAHw0CQD4DgoAaXgQAHUgDgBDaAoAgcsZAH51GACY4hgAb24SAOlHEQBEaxQAupANABORFABEMgMANt8MAH/6BwDDkQUALcIPAO8ABgCN6AsAItoOANMGFQDCCwkAseYCAO66BwB9BP//YmsGADVwBgCj5g0AvDoNACFT+P+taQgA45b7/2mm//8GP/7/wvzu/00y///xz+v/9+7l//6W5v9SUeb/rGPh/9DE4P9f1Ob/AFXf/zZ+4//CF9//Uhnh/98p5v+N0u//3Jzs/24F6/83wOf/FoPc/yxM3/8TWt3/ghHp//iE5P+ykuD/RUno/36o4//7pt3/Ii/g/1Vl5v8qs+v/qL3i/46p6P807+z/JZ3i/8vF8P8nh/T/cTD1/8Hs5/+BFu3/Scz2/+6T7P83Fer/SS/n/1h96f/Xxeb/Jznq/31F6f8piOH/w9zk/xVw6//Qdur/iE3p/z0O7v/Aaen/5sHk/5kW6v+xIOr/AAn3/1UD9P9aX/H/4cTo/4Kl6/9F1PP/miX0/8MS+/+qfPL/Pbb5/2Ok8f/m/fb/bBDv/8ex8v+u2Pr/Wonw/yAN8f+3Hfr/czgBAA9C9f/C4fT/qkH3/3WG+/85rfb/7RoBAKxyAAAvrf7/ffEEAOeoAQC0tAMAmw8LAAXJCABBOQkA0SEPAF9tAwDgahAAkIMJALghCgAt/wwAYSUDAF2KEgBBzwwAEuoSAMXzDwD2BxAA+mQWAG47CwCXKhEA63cOAEKCEwAerwsAs9QOAKRrEwB1Jw0AQCcVAMZJFQDVIBYAENsNACLVDQDKPwcA5IMGAL9yAQBrb///TrL9/17y/v91VgMAn0D7/w3z/v/8iwUAPjEJABI1//+tLQYAyhMIAG7VBAD5kwIAYSgCAHScDwAvSBQAF8MgAAL4GQAltRUA/bUeAGiiEAAVpQcA/PMJAOV5DADzLwcADrgEACqhAQCVQfv/3ggBAFZCDQCWeAsADar//8fO/P9eLQMA4+oFABQRAgAniQIAUpL+/8oPDAALSQMAsYH6/w5VBQDyCQMAWq8PAHN8CgAStggALWEHAFcOBACMewcARqMMAMzGCQCb2QoAku39/xx1/f9/UgMAueP7/+rCCQBH1vb/zSj6/0fTAAC7Efz/NTfz/ycp7P9o7/X/U7zy/zVL9P/C8PP/0BDk/z8g8f9Q6u//8vLv/9Vs9/8k1e//7Sr7//t29P/Klvj/n+ju/1TO8P94j/z/Vuz3/8km9v/KNPT/1nPz/zYg9//czPH/VwH4/3+I+f+CuPz/gSMGAPXt+f8uTfr/bX75/yWP/f+FnPz/wT34/z5M+v9mjP//XPzy/7j9/P907QYAdoH2/7CdAABK/QgAhnMKAIiZDQCyIA0ACK0XAOTJDACoGQUADkoPAMacDwBrQQ4AKuALABcyGQAHvxIAhKwTALZnGwAIOBkAIK4hAOIDIgC/tyEAZxMjAGGmHQBs4iAAZb8YAOHwHQAmdR0AXl0YAGYNHABSShcA8kcaALLuGAAa+RoArcgTAPJQGAB0dxUA+uUUAJOzGQBgEh0A0DkdANKnGABr3hQAQ4EbAKdiDwAoORMA6okdAEzhFQCJJSMAG0AcAOnvFgACSgwAcQkVACB6EwAqchAA09MTACf8CwBt1Q8AQpwJAIOQEQC0GBAAEHgGAP5rBwBGgwcAwu8QAFwRDQAzEwUA4/gLALNmBQAonQkAoEcOALJ8/v9hcAcABSUKAKDZAwCrwwYA/B/+/4yC/v9MT/f/2Zn4/3aZ+f/sSvb/hD73/30A7/8CBPj/sYr+/5Z29/+HrO3/Hnf6/+Ng+f/FJe7/PMb1/5st8P+7h/r/lc/7/8M09f+yg/T/LU3u/0kO9v89FO3/Kurr/wK+7f8E9ef/cD7n/xDz7f+v8vT/BXf//5VKBQDQg/7/4QwKAN21DABCdAcAviABAMx3AgC5Vvz/Gl3+/8Ma9/93IP7/vlj9/x0tAAD5YAUAXdr1/6Kh//+6gPn/q2T8/6y9+v/PPAAA9wD1/wgh+/95SP//ZKX6/wiuAQBF5AEAuhMFACWv//+5mwAA00EBAM8P//+rsvn/ZGb6/0Rz9f/ZLfb/uLX6//248P9DdvX/gLL2/53O7v/lD/L/TrPk/2nk7v9aN+3/T7P0/2bn7f8C4OP//gvp/7GN3f8iTOP/X3Df/48I5f9I+97/FcDf/wIJ5//9wNz/Ptra/+Vv5f9Pgd3/1AzY/6qN3v+L/9n/xtre/8nF4//7YuD//Onn//Gl7f86uez/C33w/4Px8v+nP/D/QEXq/+qg5/+6wvH/gAf0/8bM7P9EjfD/hF7x/+dx+f+eE/X/8ZwCACOeBQDrTvz/ybwDAPRPAAD7o///N3EFAIBwCQDQTfr/L2gAAAp/+/8Bovn/Ryr6/8Ui//+LxQcAfR35/zFJ+v8s5PL/1Dnw/1G+9f/oSvP/s1b4/wyv9P/P9fj/VyD3/4ml///GHwMA4rT4/0npBACb1vb/tg3//3P+AgCmMgkAyeIJAKbTCABMlQoAnmALAGXeCAB8PgoA6R8RADQMCAC49Q8AbNkBALzbDAAaHwUAuDAEALS0AQDTVv7/Sf4IAHN3///U8QUAnCwBAHqIAgC9BQUAbeQJAA2QDADZWAMAdYMIADQcBwAsBv7/i237/2MF+v+auP3/HJ72/5gV+f/k7fv/0UP3//wp9/9s3fr/3O8DAHfi+/83vAgA1rsGAKw+BACJJBMAFY8EABKtAwAWpv7/h8AEAHAvCgBvaQwA4VUJAK+6BgBYagYAyZMDAFs6DQBl0wcAoY4MAHRMEQBJsgUAFUkGAGGdCgCgkwYANy4LADxfEAB+WA0AfK8KAMvFDAA3XA0ANxEIANDoAwCgqQcAwvQOAFwHDwAXiREAzF0QAG3qBwDithEA3M4LAHYqEgBL5AwARwsRAI4dEwArDREAT0IdAOYREwCDQxgA0H0LAFO+DgAy7g0AP7wHACdkBQDzAQwA50IMAMwYCgDm2wQAFqb8/yFiBADC1Pf/QE/1/yc++P9DBu3/8Xjr/+E88f9mzPj/baf6/6TB9P9rBvf/+IDu/4837/8eseL/y4Lk/3Dx5f8d8uP/AuPs//JL3P9v9t7//Nbj/2p35/+yVu7/tH7o/3yI6P8P6ev/fNXm/0TU6P/vN+b//Ufg/9bh2/+yhtr/tWDm//uZ4/9/IuH/RLXf/9pt3f8HEeP/zvbj/+B67v99T+3/0MDk/yEv4v8Opun/HG/p/42u4//Tvub/LW7u/xj/8v9zbPL/DYz6/1qc/P/dcAUASOEIANpCCAA5kg0ATR0HAHrxAQAPrAgAHj78/9MqAQB/fQMAYAPz/5xE8v9dBfP/e/EBAOow//8wbPz/DiD5/x697v+aBPL/Jj30/59N9P8nV/v/cd/1/+L79v8rK/X/kTbp/xsT5v/eCtb/RsjC/+c6rv8KtJ7/Q3J5/9m1WP+aSUD/j1Yt/5MtL/9cyin/Ptcs//VgPv/uGlb/Z+p5/yqDqv8MN9f/rZ4JAHU/RQCLtXEA4/KjAGb42QAMIvMAU+gMAX3AFgHRcRsBNmkkAfevGgEaRA4BJLIHAa/Z+AC/idwAQ2TWAHWGxABRh74ABXO1AP4aowB/m6YAQaOaALa4lwADqpsAzkaWAOL/mgCK5poAi2ONADOnkgB+yIcAkX54AKAkaAChR1EAHWg/AM4WLwACAiEAya8OAIsaBwDQB/7/rmXr/9NV5f/RPuL/LcrP/3zlw/+l+7v/jRPC/+Pcwv+qYcD/df/D/9foy/9AY87/CxTZ/8QL7v+hnff/XiEFALQ8DwAbZxcAWb8hAL92IgBk4yEAeeQiAEF+EwAHrQgABDL4/+BM6P8KJ9T/uX7L/8r0wP8aD7f/8Pq9/wfHuP97Mr3/n6nE/0//1f9iTO3/w0YIAB4lHwAqjygA4bcuACrrNgBO6D4AZmU7AK+/NQDdsSEAsbwNAIpE9f98EtH/8rbC/0yVnf9KDYr/MOl9/y1TYf9JIU//blA8/+nGMf+zliT/PlUY/2i0F/+6yQ//FGoI/+/w+/61quz+CnP1/lsm7f5pY/j+M6T8/qapAP+HPhv/X8os//njQf9ws1v/4l9x/++gmf97dLr//V7k/zf5BQDfWBYAnA88AK/1TwDmdXIA0cCBAMWgiQAQxJIACEqBALgNcgB6OHYAUixqAOxDVwBJIUQACak3AM3UMADGFi4AVNItACY5IgCtFScAyxMpAMn1OQC8DkoAoTlFALEFTwC0aFUA2sRTAEMQWADyr00AN8VHAMd4SACyoUAAwlQ7ACIoMgC6ZyYA2UUXAAwpEwDgJQwAtEMGAKt+AwBcv/b/K9ns/5wk2/8Tztb/4BTS/ya60P8JQtP/WfrV/x+g1f/Tts//jlDW/58q0f9SKcr/hn/I/wyZy/8z187/TV/A/4mkzf+B28//kBvP/1sJ1f/+ps3/ebnT/3HU2v+2N+X/UCbo/55z7v/5MPr/UwMNAPhcDgD/ABsAa7gfAB4TKQB77TEAnZ4zACpoPwBAF0EAWOFWABUJYACvDGcAza5oAJuWZQAIRG4AldBpAD9uYACMj2gAW+hrABL9YwDGWGAAdSZbAEcqXACiq1cAx4RUABEDXADNKUsAF1RFAOCfRADrnD8Acpk+AFEpPwBXxz4AAGU1AN/XJAAg+RYAyc4XACnNEQAV+wwAQEcMAD1pDQDfPwwATqsUAFc0DABSlxUABRQhAE0rGQC4XB0AJNIeAOsDHQDfzxgAc6kYAN+iDABJ4BMAmYsVADUtBgAkfAUAR6MEAK/gCgDR1AoAhOERAFQQFwC85wsAA7EPAFHJFABjCBcAgHkOAO0+GQAEUB4AKVMSAC/uIAA2XCYA8CwhAPgyIQBQrCEAVGMXABkLGgASghMA/ucLANZw+/9IZu3/n53d/4S0uv9G36f/FMCT/xgHnf+1X5//xTCo//puqv8xIrL/guOl//gfqf8UyL3/rWvA/yZP2P9TcNL/y3nj/yfo8P8T8ff/VmUFALt+DwDFxSYA4IYsAEMEKQALICMA10kMAMxvAgCck///HYj6/2Je9/9HJf//3ssLADXK+f8jguX/6ibd/4AM0/9RTO7/9gvn/2gv+v8VhhAA4Wz9/59ZCgACA/P/4Yby/zRV/v/t3v//DdAWALUJKQCZix4AJmEfALKpHgAn8BkAGD8YAJ1zGQCcaBMA8LkKAMTDCADaTfj/rQEFAB0H8//Og+n/60Pt/zIY0v9x+tn/hyjR/y0Z1/+rtt//QXPi/6s48f+3+u3/BMXg/zeR2v93aub/IQvT/xbrz/86sNH/Z7LI/3hoy/+jQsn/OxXQ/1ABzf/fJ7z/Q7S+/3+2v/9+Sr//tuXC//EZw//Ap8j/1z/I/1uQw/+PK87/ZAXN/+Inzf+HjNT/hGjc/8LW4P8hDtv/1DbV/5S33v9BxvD/jvbu/09H7v+Ti+7/12b5/2zJ+f95o/7/woz+/y3zCQBGBv3/0MH2/y1X9P/Beeb/a5fw/z8U5f+29vD/bvz6/3Cn/v9rKRMATWYHAJRqEQA9ahwA4k4WAOPfLAC6YiIAi88nAJ+fIACy8h4A/ssoACgrLACkHisAVGIkACl2FwC8rxQANtIVAIwWCwCfBPv/5k3n/+uf4P+5GMv/1iPM/5umwf/xJbP/Owix/xAYs//lsrT/uRez/0XdoP/aBaf/oHOu/8Urqv/8nrH/W7qx/xmAt/9ZwLv/Qhi6/x2YwP/37dH/VqnM/2oD3f9QCef/ebrt/1239v/tYPz/WQABAMBb8v9uR/n/RHv0/140/f8cXfP/xivu/wCJBQB7uvP/9V0EAN/wAwCJHAAAPEYDAGBo9P9xf/z/o+T8/310AABL8gYA2MUNAC1vCAANmQgAn80TADcCHADFhRUA9KcWAMrqIACXWS0AXwM0AM1DLQDABioA3GkZAJ5AKQCBDCwAz58kAJb7KADNtCQAEr0vALCwKwD+jDAAlZ8mACktLAAw5y8A5NosAATpMAAv1CMAh4wnAJp2EQCdOAIAzqP7/xfW9P+K7fj//jj9/4YuBgDkLQEAySQDAPAzFgBzBRIAMv4VACyRFADWLhkAKIUkAKLgHAALLx4ANB0gANzSKgCgki8AQtIxAJWcOgDVCEYAMTRJAHcMTQBqNkMA4dJLAMFPRwBxG0QAk0RGAGDeRQDQjlYAPXBTALO+VAC5aEwA0MlNAGa1NwCzgzEAfwcyAPh9JwCHSSEAf/0VACWnJADZrRoAeWAVAPXmFQDDcgoA/H0VAGCsFQACTRsAOi4mAIZiKABw9SwAbB4oAHvhLgBxDCMAzpEWANG8BgBTTAcARegKANYn//+g9AQAqvH+/x8LBgCgtgIAQLwFAHn4BwC6QgAAvPsIAKOPAgA4sAMAI1sDAKLwAADwmAQA7sz6/3tS9P9W7vf/Wlz6/3ID9/+x3AAAIyYMAINYDwD8YxAAyHkLABGSEwDF9REAykYhAGVVKQDIjigAEGxGAKipOwDIiUoAj55IAJ69PgCQ7EAAxxMvAPr8OAC+Ty0AxeU6AMaVMACM0CYAM/kxANq0GgDuFh8AExAUAJWHGwB7bCEATgcXAHBsIwCQexQAekIPAJt7AgDKmwQAylj8/2p37v/d0fP/6Jru/8kI+P9vZ/n/NHf4/xbl9f94qPL/7g/W/zk7y//0o8P/Ad6p/85Asf/yLaT/BOqo/444p//v0p3/eP2t/zPYpv8RdKb/XRKo/0xas/+gKbL/86q1/9uMuP/Q+rz/GuLH/zmzwf+XMcv/9XbG/yd11v8a0uL/M5Dj/1rq6v/Jce7/a1T6/0g56f+/NfL/10ng/95b3/8iv+P/bSPO/zXV1P9p78X/FyrJ/26PsP+Z0cb/9Qi+/6bnsv8gobv/aeG1/+Wew//c4a7/VerB/1Cysf8/sr7/X9bC/4QSyP/EwNX/nArF/y7u4v/qgtT/tqfj/8Bg3/+Pl97/KmXw/6Hw4v+DFfj/BKvn/5QB9v+sVen/0v/y/yBv8v8VWvP/dKz5/9JY8f+VjQgATZnn//9nAADSi+r/AT78/2W28f/6DO3/MeHo/4ZQ5v851wAALBbx/x0AGABRcPn/MpQRAD3CAwDyqAoAtekBADA48f9EQPz/MR7w/0sXDAACOO//jxH9/5Zb6P9HnPn/CLoDAFTEAgD3nQUAsyfw/8gACAB1zQIARigYABkQ/f9OsgsAxe0KAMPkFQAZ7hMAdJYAAMcpHABAkg8AO0ApABomFwCHDysAb28aAGFOHACzWxUADSATAPKEEwBSvvj/Y0ARAMiq/f8KaxoAn5gJANRXHgDqfBAAAVcRADOIFADcqQgAzusfAO1f/P8RRBoAeo4OAJW3KwAk+x0ANMIbAIgFIADP5jUAreQ4ALEuLwADEk8Acg46ACA6VQD4XzQAPUtGALmoMACZVToA5nUmAKKLIwBqxCsAKJsnABP8PADFETIAW15IAHS7KwBBnEcAG9spAEEcPgBylx4AhYQlAD3oIgDC6h0Ant03AFFrIwAfsDcABqQgACxcPADwNRcAobQmAAouCADO0Q8ADpAAAEXNCACf5AsAQRT2/5RyCgBhS/H/cA0OAC4Y3f/hGPn/6F3e/0hK+v8LJ+r/9Cj0/9ew9P/qZez/hNDr/6Dj0f8gvOn/huzB/5AI6v9NtsL/fsHc/2Amx/+8ouD/DrDg/1f46f/6Ztv/5obT/5mk2f+NKcr/SSXj/08ny/9kQOj/nRDT/2YUBgDqd/D/mfkGAKan6v9n9AEAOU/u//g+8v8jRvj/rnPg/0Ls7//fttP/Y7j0/5b10v9OAO7/I9fL/1vi7/+QAeP/dV71/1Wt5//58ff/1Pb0/5te/f9ZhQQA6lH+/3l4BwBCmfv/bKceAPkB+/9KZSkARiIRAGl6NADh9CIAukNFADhuNADzUUYAxRY1ACH4OwD30jIAXHExALkEPQAKliwAj1hFAFpZJgBCAkYADK8vADi3RgAZtR0AWiY9AG/iGQBiaD0AhNgfAIY+OwBErCYAmlIrAO1WIwC/oBIAe+okAFZ8+//r2RAAsH35/9woGwBOJAAA4pMZADIc7/+2Lf3/lLbk/+4r6v8FaNL/Y//d/+C4yv/v683/YMDA/6L/yP/O1dD/hPnG/8qeu//uBbH/mwa//7ZXr/9PL8P/fGen//V/tv/JNKP/bB+//zk5q/9DhMn/552u/89Dyf/6kcP/XTje/x9Rw/824cb/YEXL/5sFxf+9ddD/YKLF/2QjzP/T8MP/SYbN/1A2wf8R/tn/lzXM/zxC4P+6LcT/Nyjh/4ri1v9Si/D/lejg//T86f8R/97/TiHY/60Wz/9mBdv/NWPY/4aX4/+Wb/L/pUXq/whT/P/2SOr/qnLn/2vZ2v9vOdr/Ckbm//su7v9ZZvT/vTr+/1QQ+P/3JggAfcn5/8LrBgA1t/X/GSoIAMH1CQC/XQoAg0oQAK4QHADiihcAXHgpAGWcGwDVLRwAEkoSAHBQCgDfuAUAjAX9/wRlDQDHOgcABasPABM+FQB7dxsAYwEaAJ/pHwB++xgAd7sfAEGAEQAPCiAA5YgOAJb2EwBBNBcAZaMcAMiqFwBlcxoA/hEeACDjFwCZRAwAlNcAAFnZCwD3tAIAInoLAIPWDAC/MiEAnOMjAI+oGwCTnxAAkt0OAJBiCQAxGQIA9JgJAJ6UFwC7PhkAkkoQAH3EDgDwDw0AjtIPALt/AAAG7fz/l3Tx/1LoCQDy8gUAG7UEANsiBwBR1gAAPNQGAGSj9f8CaAMAiaT6/6d3/P/6yPH/o43t/y4zBABa6fv/G0QIAJM8AACTs/X/xfDx/2nI3/869un/pvjX/06q7f82S+T/P7fv/7Uu///pn/j/GFUaAHvvAwBd0BMAb68IAJwXDwDHJxIALCQMAFtqFgBulQcAGowdALHWEACH3iUAupQtALjTNwA6bUEAvElBAMntTwD5j0UA/xZaAJW3VwA2f1YAOMxZAEzWWABvp2EAlORcAD2CYwB74VwAr45ZAGAvZADMiV0AzChXAKERSACX2DsAmJtAAJV6TwCyLT8AQ1c2AM0PJABv4hQADRYeADcTFACyzCAAW+YaANGjJABZtSUAMWwoAKnoMgCfjCkAmjcpALiCFgDxTSgAJcMfAClVJwD83SIAIy4aAAPILgAaJSsA+SZAAA3eIgAByxUAQTgIAIiWDgBkoxEAw1YGABvoCwARPu3/Nv7w/1Br9f/GZAUAF3wJAGk58f9sIe3/a8jx/yJ///9AX+7/DTvc/4CA0/8t1NP/ZePX/zQt2/+2teT/JoHZ/2ql1P+zHsv/BmLa/9RT1P+bKer/RlL7/8Sl7v+qJgEA/P8FALo7BgA2XAUAIb0QALM8AQC6hQUA3RH8/+5E/f/ueAUApjoJAE3DJgDkLxAA0bv8/42hyP97lbj/EIuv/xv7xv+939n/YiXo/0JlBgBsduD/Vn71/yHf2f9pW+7/Uajn/ylu4f/DCvT/xjb3/089CwCkV/z/3hMUAN8D/P/nw+f/GLDU/75H3v+t+uD/BAbs/x5q1/9nJ9n/asLp/5v31P+yzvP/QPfs/+fdFgAALhcAJy0cAIc0GAAtFAYA72UGAIzoBgAg4icAApQQAIwCKADj/hIALT8RAI2MBgCZz/f/EHQIAKyiBAA6Fw8ABELo/xiJ9//+yer/wCf1/5qV6//olub/8aju/wWR3v9lPt3/sbLK/6r75v+TFOH/ihDn/8eywf80wdH/2S7A/xNhz/8FlOD/4Z7g/6TOAwDerOD/vqT2/9di3f+Z/+3/Pbq4/5RWzv8J/9P/YKS8/8V4qP9EEIj/uGGw/z0Qk//+s7T/2c6Q/2bQqv/TA6X/or6l/zv5m/8V35L/yuO3/wQ0lP+fGLb/JVKS/3TnvP9KnYz/ZRq1/1JTpP//abP/sNGB/0WUEv+LyKD/MXhw/ynvu/8ZZbT/Gofs/1MZ7f/JggcATjCY/5eMzP9Qy8P/CHX2/sAUkv85LIz/62/s/2p98P88e/L/8gDd/yyCBgCCBl//3HOZ/yepqf9WL4//Pv9BAJck8f8xPmUAp6sjAHl0AwBzeiwAjwM7AIJm8v/hu04AR9kiAEcbWADHaH0ARf8WADFAkwDXjlcATaJdAMLCsP8YE+T/7kQGAI4F+v/eAA0AzqtAAARPZwAJPQIACegHACMP5v99NBgAEwv2/x7JQADPiU0AWyF8ALSXiAAIbGwAYxloAJ6IVgDQzm4ARGdqAPrOTgAC+mcA2RuBAMKBUQAw7k8AHaVSAIhNkgAclzcAaddIALnLJwDtjEIAyAEbAF628/+/L/L/8YQAAP8WEABNxbL+vk5H/0L9FAAnMAMA/g4KAMZoSAA7WIkAezIaAH2irf/3s3z/Juxw/8lsnf/Lt/T/3dH//wcBaACwykkANawNAFN6BQCuC6r/xurK/wNY2f9YBNb/H7BgAFH8aAD9EV0A3pZEAHdCRwBhx2cAM4/q//H5JQCK7vj/1Uf0/8b8MQAcwvP/xnQGAK+HPQCOpj0AKgQ7ABJfQAC/pwUA17MdAPNc+v8TsQoAHF4MACdqTwDY+XUACe6EAFNwXACnCjgAciJcAIXeIgAiySUAKbAWAMdnSwDu20EAlMeLAIMZggC87poAi+KIABzPYgB5G1kA/bE8AGCJVQCtDzYAZRJtAF+1bgD1jCcAWhcEAHL7bgAxg1wA7eMhAN9vCQATdA4AJxv5/7F91/8TMNb/PQUYAHRBCgAhxPr/PlE0AEYyKwAL+y4AdbfF/y1hz/97+fT/GATZ/6JB6f9JKQ0ARMgoAK1FSQAS6yUAsxUyAN/CEQAZS8r/e7bs/wWY5v/Lg/v/Gp7G/6sc4/9Jk/v/9kLn/4HJqP+uQ1r/NEWq/znwqv8VH7L/iMTZ/xCJEwDNPAMAvjrM/won+P/rYeX/BWzT/5Tu5/87jRsAQQobAG3MMQDAYTgA32ElACsoVgBGGlsAQztdAFXLUAAOUlgAbUZeAMdJOwAfCz4AiPBRAPIoQgAhR04AAjQtAJYhLQAzzPz/yqIEAJWa+v/0yun/rq8OABp+/P/dkRoALbsCAJbZKgCIghUA6mgnAIVdEQAVKvj/P0v6/2ycEgD7HhEAXQv8/1SIBgDKTuf/sgHV/98vuP+zNqj/Dtag/42ou/8ULZ//w5e5/2y1v/9eGsz/S+LR/xT2sv+DHbT/9iqx/yVArv9EusT/DQ+4/zVF5P+tAcL/OO+Q/8tvx/+od77/KXOn/+uTtv+3X+P/Xnuk/6+4zv/u8eP/WKC8/8IJuf8Eg7j/UBW5/86esv8VkMv/V5jC/1oVx/+JScH/g8bQ/+LPyP9kFuf//Uj0/1MyEgBhqv3/awne/5HV5f8g1tT/OnW4/6L20f/xWdv/G7LV/3YZFgCd9+H/zfbu/2Gj0//1NBkAuqJ8/8sNF//ph77/MBJEANRpGABBBbr/NA02AIOp//8GKH3/UAF//0MMrf8tyGv/odLW/3Oh8f/0VM7/9htVACwBQQDjI/v/jW74/yjjEQAW6OT/3Ei8/33wIwD3tg8AhaMsAGfvigABCnUAZWdVAFeCDgCJWdT/7zrl/+4z3/8TGNP/1Xft/zgCEgAxmkAA/fwqAKEzRQCZGyAA7azo/1Qi9/+FbuD/Iv8AANGBIgBEszMAXHxrALKOUwDmBiMA6ogCANSs6/8ICdH/dy7c/2e++f+kASQAreMvAMRBNQCzXlYA3xQhAKv8BADgMLf/+Ya//5nFp/8UY9f/xiDf/+e4of/uh8v/P9s2AHasRAAs9cr/cPIOAHVZ4f9jS43/+yZ9/2wYvf8/5tn/DZwEAM0TOAD9EFkAwiw2AFJ94f+OH6r/G//N//eW3f9a9cX/3SAqAJsxYgB/6W8AlJZOAOBCSQDsygMA0YoSAMD/+/+7M+//jpQLAFarOwAyH20AonBvALjfbQAuckIATXFCAEtFFAC98v7/URjs/7kfDwA510cAKipBADXLTgBSI1IAmE9IAIpIPwAn5v7/YZP1/7HwCgBFARIAkZQdAHTHRgC//FEAOpdGALV/RwB1EhkA6+sKAATt4//W9vH/+wovAGoFMwDalSkA86YtAMT4MgAuEC4Aa/QeAPx86v+xftv/5ojz/3EK9P8y/iYAxcZCAMbFLAAKw0sAE0dHANIbKADJWx4A/3gBAMl/7/8NAvP/x6sOAMtgBQAR8gQAjeYtAOPSPQAt0SYAXNzT/wtcqf9Zi7n/RFTG/xMIy//VMeX/TQQDAAoOEADVqRIAH0D9//wt2f+2fcL/il6y/+YWuv9AiuX/dyn8/9VvCgA48vf/XIkBAEnlIQDX9Pr/uKPR/93Z6P8dUOb/1qjW/0Zk9v85FP3/xSX+//lj9v80u+v/gbjo/0sm0f9Gj7X//YS2/4hTyP/vHs//scLq/83oEADbXhkAf48OAM0a4f9tq7f/vYfJ/7B40//x8Lv/XT7l/4IfJQAxJjkACA8fAAcECABS+fL/aOrh/1X74v88kq7/Suak/1cz3f/d3RgAXhNAAOG2RgCIwhIA21f3/5oB3P+k4qz/h4uY/xC9lP+eC8z/sZns/0ZkGAAIqS8ATxoEAFPk6P9efsf/FnKw/3qVsP/718H/NtjZ//Ns5P83dgMAjEslAL0lGgDYewEAMhDt/4Qp3//+5tD/YvjK//ur6v+RJw0AuzIwAMInRABQszoAqsIDANIB6f+madP/VUTU/+Di5f8+0dz//bUFAKAUJQDCZTkABGM3AP5mIQDlIvP/O3/f/6Oi2f9eV9b/fAD5/0mUDgAj0RkAA3w/ALzhQgBQDREA+WUFAP8tCwAz+hEAlmoDAPPV/P+PJiQAzztTABIkbQCPIlkAnXdRAFVJUwDZcDwAvXlAAE/oSwAM0kgAVZhXAPfYegAbHmIAa7Q4AIORNgBBiTUAKcoUAL4pDQBSawwAKc4MAIg1LgB+mykA+Xf7/7Ns7v/89AYAfcz7/5dG/P9Kg+H/YLXc/2II3/+SAfD/p6ANAJiqBQBUNgoArh0SAGixAABgmOX/xXLx/04KCgCGcCAAvHYZAKxVGgBnuCUA55MoAHOu8/8I6sL/AQLL/xow4P+Q3g0ATj4AABLG6v8AoQYArBwNAE2jBABurff/xy7Y/0jI6f+pkPb/uRzX/1iXs//ykYn/0Xmn//2M4f845Pv/2mjy/+a7+v+8bQgAeyD1/w2mzv/2rrr/xZ64/5TVw/8Aa9r/kHzt/5rd7v8TbQAAWacIAJhE/f+2IO3/jpml/ywt6v9hsgYAlSm4/xSg8/9WcEUAxv4hANoO7v+XDCcANTv//2v9qv9GEeD/KyLL/zWdw/8wwBUA2uw0ADVFLAA3Z0UA45BMAHxg/v+vue3/i2T8/ws81f+pE/L/k/AwAM3XKwBuTigASCM9AOezFADrGOH/nn/e/69auv/Jc7//fW8OAJ56PAD1zi8AqSUzAOrEJwAbTvT/Q8fk/0sz8f88oPP/SBDs/xnLDABiLRgAWawSAOzpSgCkiEcAcGwiAEWFAABjIPD/dbP9/0DHBQA1cTIAcKALAL4++v9+Cy4AnVYsAEL0HgCQNgQA8Xr//4SZ8v/SJfj/srgCADQy8f+t8gsAOqwRAKcP9v/ZMt//nyzk/4p27f+CS+r/DBHt/yAp+v8UZAwA97sNAIQMAQCKwfr/HVHt/wwM8P/5Bdn/6/XU/1cz8f9eIQQA2o4WAPw0BQBWYgYAIuQQAB/uFgCqJAwAB2UnADQGLAB5CCUAtAMgADiDCgC0NxMA80QLAPhJDAB/ChMAAYAcANt0IABXLBYA/xwMAOFPAgB8Afv/+8YSAORXHQAjQBcA/FgkAL1lJQALPRcAd93r/7Hfyf+5vdj/883l/+Mk9v8FzAMAVMcEALXZDgDTOQUAZNPz/zMl2/+d9Mb/603C/3n/1//75+3/4mD8/8n9DQBMXQwA0dYDAIv/9/82dPL/hL7w/2TJ8P8P/en/j/z2/2Y1CwAo3h0AMkwYALUhJQBCfRMA7Nr8/+TW+v/pcvT/qF78/62e6v8thN7/gU/b/2Up6f9lXef/ks0BAJvlEgA3ICsAtDY1ALaPKwChaRwAdl4BANyW5//Zvtr/aSjz/58sCABXER0A3HYzAEHRPgBFCSkA1+UKAJXG+P8ke/n/2Nn2/1e78f/1fAgA3jQIAN9vEABMFCAA9z0OAHl2DwCp7Pz/AmkIAOBnAQBBaPX/4e0AAJXm9v+8vPH/eJb8/51Q9/8hVOv/Dr/d/8ay5P8jlv//1/AJANMJGQCOxPj/DFLl//U25P/lNen/1VXW/1h10/89cuz/+AUEAERPIQAyyycAaxoZAA/69f+JYdb/8eDT/3+v2v8xCfD/GqH3/0U39P/5TRAARAkWAMX6FACxg/3/K2Pu/2RN4/9wft7/hzbk/10S8//wagIAtQEQABBFAgCkm+n/JFIHAPFG8//ztNP/NLDT/2hG3f+fUd3/GS3c/xu85/+MBu3/EkXr/0tz5//9z9r/mHnG//B3v//k+uH/WOP0/xU/9v8VkvL/VxXz//ZF/v+Y5fP/X7vo/9+q0P/VYdn/p4jP/zhs3P+/B+3/0Q/1/0Gx7f80ic7/Q1DV/8NmzP95ccr/0LrY/+Gh2P/gQeD/4Djz//TJAQDuhff/8SHv/5/E8v8QHM7/d3/P/1dT2f+9De3/58vf/85p4P+Qeu7/3g3j/wgD7v+Ue93/fD3j/4P83P8jf+P/WAXn/wkI6P8kBvX/g8Tz/wnQ9/+hkgsADlMZAIkYIABCECMAoa4hAG+uIQAGBx4AtM8oABvLIgCkaiQAE4knADpvEgAPgxYApv8oAK6XLQAylScA+PwmACyrRAAPkTYAvrwWAPAjBQDS5QUAtdggAH2UIQBoOBoAvrQEAGeIGADzsSEA0CQPAMZ6EwBwRQwAocoAABxi+/8lHvj/ekLw/yCL8f+Mh+v/aAjx/wXfAwBw7Pz/BSEKANkkHwD+JBUAxqsPAEjy+v+TAO//gA0GAFENCgBNmAcAYksdAN+VEgBfVQgA+sH//1KRGABRSjYAIlMpAFziKwCS9CkA5jYZALXV+P/hIvf/oDn9/6xBIQCxui8AUOgpAElOMgB+3ygA9uoNABg39f9NOfL/QDvu/xjv7/+Gv/H/CFYLAFMKGwDKsxcA8DYCAALkAQDr//v/1Xn6/2FAAAAqR/T/WUj2/zs08/9MqP7/czn6/8Gn9//SOgIA8n8XACpDEgALlwMAnBcAAA/z9/9yWf//0Lf2/+bv+f9N7gsAdmkTAPXPCQAgP/7/Yz73/35p/P/u8AcAcQkFAG4GCACNggYAhkv8/2/F8v8gwfH/yB/9//MQ//8qugEAP3D7/9488/8swvT/QVnl/wBr7f8Mr+v/4jLs/5Ya6/9cPOP/iL7z/xps6v9xYvb/qlUAAF9YAAAuOPv/ZTr6/2gJAAAw8wkAu6kQAMbHDQCLxBQA1ksZAGo/GQC0GBEAa/IwAIIeNABbpjMAGPA9AFeNQwBv6CwAYyYLAG+RNwCXpFMAo8JTAOM+VwC0KmEAolhfAAxQYACapk8ARwFJAKLoVQCEVlcAw09YALt+UQDYPGEAvWV7AOXvjQBSBp0A1iueAHlOjQCyDowAWdF3AGcybQBqeWUA3AhrAA41cgCE2YUAukKrACNzqwDZ+KoAKFyRAK6mfQDU+nMAxaNtAFLIfgDfE4wA782hAH0JtACymL4Av6fVAOr92QCOzLUAJAqGAPGFggCZ5X4AzxyCAF2zogAIfscAFqvKANgRuwBsK7UAeJ2+ADCmtADdbHwAs+lDAFeLOQDK22MAxVuIABfXnQBjV4MAqS2FAKb9kAD9t3QAnLxxAE2fcgCfRWEAFQdSACo3VQBaRGoA8AJoAEWpWgDr/0gAd2spAGUsCQC3OQUALAcKAFJICgBwaSIA4b0UAF26BgD8CP7/bTHp/xymyf9Zurv/P2yz/7pyk/84+IT/kyeC/yT7gP/cLnT/guNZ/6xEOf9DJhX/C4QB/2uc+/5Lc/r+gYL+/kLVC/8loPz+A8js/hqL/P6aDe/+RWzp/vBR5P7rmtr+bUbh/vYp2/4Gttf+oNLS/gQ0yv4DIbf+Bqih/i5Pov64RJ/++Sil/hNFqP6V0Zn+rayX/stgj/6lFpP++0yT/r9Ld/6uOHX+AZV+/pODhf60LoL+xPiI/tvqlf4KT5/+uuar/gIGrv7PQ63+bWik/gY+nv43mqD+N5Of/tH7r/6XlLb+weer/sJ4rv6py6b+GsKw/rlwnv5Dkov+2K6S/qEilf6I9Ib+SMeI/gkrmv7SdK3+t5a8/re1xf6rO93+kgjk/hWG8v6FOOv+Qk/Z/gmyzf51jNL+o4rU/m6N2P6+Atf+bLLZ/prS4P4skd7+xgDk/pDE1f7uxNX+pevN/vDg5P45KvT+cm/w/qjtE/9aMzT/L/JP/zPNWf+XE23/J75v//A9Zv9V4F3/fc5a/+2Lbf8vnoL/sUiZ/x2Epv/JwbD/Y9u8/yS1uf8//af/3zmf/48bpP+1bbr/6dzT//e4+P+BsB4AwYE/AJ1ZSwCrKUwABBtQABjLRgCoYkQAXTdbAINmdADpKJQABmu0AKQPzgCOFdMAtZjIAG8rxQCbNsIA6Q/GAKqX0gAKHuEATFvuACs3EQHZGC4BnWc/AT2EMgE4VzEB/eU6AcHpMwFFVkUBgvpPATR/cwHny5sBwgChAb+4sQHI0L0BVGXBAR3MqwFGsp4B/nCdAfvmpAEHhroBn8a3Aeh4zwH37+AB00LeASZi5gE+vdgBK3DWAf1n4AHE//UB8XoEAo958gGxd/kB6pv+AaXWAQI3lAACNRn/AURNCwKzbQoC8XkEAnwU/wGIuOcBcijwAbbQAgKFAgkCyGYdAq3LIgJBlDwCCQI7ApqwIwIGpQ4C54MAAlRPBQJReAkC/akZAtPXHwJjuSgCwnIiArBPFAI+oPsBfOz5AXiT/wH78+wBGxr8ASfWAwIz9A4CGa4OAkX2CgL7+vABeLHUAZ5/2AGxbscB8ZDHAeeAygGQX80BBaDNAb0HvAFDkK4B4OOIAWQiegHxlncB2+RkAR6YYgFgXlQBn1lOAe+9PgHYNi0BXJAeAZHECgH0TgQBacfzALF79ACVvOsAcFvdALFg4gBIV84Afwa1AOlQqQBp350AZIuTAAyvkwDwaX8AS71+AHWXhgCdnXgAQNNiAK47RQAysDEAG+kTANVM6v+uU9H/WZjC//Jltf88S7H/wrSv//D/tP+IGqj/kf6O/5Kxav9eK1T/Oy9G//qWQf+0NDr/bEE6/4kzPv/vUDH/6Tci/59RDf94hvP+rCDc/vmT0/5uVM7+Ppje/hh0x/564b3+Vpa+/oyZuf42ab7+9c2k/v9Wmf6P2X7+n7Br/iuvX/7wMF7+wwRb/kA3Xv5QOHD+Wlpl/sFMTf417i3+nfYb/vpJF/6tbhv+u+ki/lkEM/5X1ET+JeFR/rtqVP4nqVz+rbVX/s1DRf4KgTH+C/Yq/kfaH/6BVw/+lJwq/rPpN/4owEL+Qwha/n9QYf6wvlX+J+s+/gsFKP4Vrhb+eS8R/lMKHP5ifSn+Ug9G/uszZP5+J3v+Np6H/gy5c/4aw2T+ZMdk/u+RXv6SCHH+eLJ5/tIpff6hIZP+7Iym/iGSr/5mlp3+xgaV/i7uif5UV4/+SBWb/slWpv7Qzaj+Op+t/lpjvf5TtMP+r7e3/gvOrf4ALLD+70ej/prKoP4E86/+aHnF/pgE5v7+aAD/VEAL/1bQC/+LsQH/ikn9/gUs5/4u0ef+XvP1/umlCP/koir/rEk+/y/QUP/+W07/QeE+/5K5RP8Dqkz/J0FW/9GNZv8DfHT/nNR8/5DSkP/Oeqr/lTGw/z7jq//RPav/byiv/x0zp//J/J3/SiSl//k0vP/+G+D/IWjo/x489P8Pffz/ygoBACQ3CwAQCQMAn1QEANw5BgCOkCMA0BInACgWNgDJHi4AjYQfAEkULwBNoTEARVtBAFE7OwD2JUQAJT5NANfvUwAyBWIA5KdSADaXVgAjO2AA/QFaAFDHXQCR/lEAAFNQANEvUQBdX1kAJoRoANYQbgCPMm8An5lxANJubAD0emcATcFqAPojVwAVSUsAXoNdAHZIcwAQhJMACyKhALJFrgAkVroAHxSjAOp4mADX6ocAdEx7AKOzggBuY5sA5XGpACdnqABjerMANCa3AMd+sACHsa0A2gCQAGKObgALvW4Au/B6ANZ0iQAQUYoA8qSVAE4NpQCydqQA1dl9AATjWQAEVUsAOp0+AAIAPQCzzz4A1XpXAMXMdQCLT3kALluEAEtrhgAUi2gA4PFgANxNVQBqK1wABwxjAOW/awAGQ3gAvz91ANDjgQDUuHoAgiWBABDZcwA/aGcAClFoAOyvZgBue3EAZWVwAJlsaQBddF0AusVeADwDVwB74EUA9AM2AP0+PAAY8kUADWVPACn/RwAMWT8AeJZBAFHpOwDcMCkA+9wOAHE4DQDKFwYAisQZAOMHKwA7szIAMNQ1AG6XOQAPUS0AZkMbAIfXGQDw+CYABOU5ANUVOgC2nkgAMR9LAPcaTACamUYA2+g7ACLBNQAU8T4AUPI3AGUsLQCtry8AnlEfAICrJQBbWygAMXwYAE0oCwBuxwYA2c4JAHfk//8JpQEAUIgOAEtoEgCB5RYA7roPAJEZCADV0/v/KeQAALAZAQC0jPT/dr4DAPjECACYKBkATEwcAON0GwDLXhoAUyIXAO1NIAAnXB8AC5sbAClWDQCyaAIACm38/yBFAAC7/wYATCoTAMUsLQB6zTwAD5A6AGdpPQADaycAl2IQADgu9f/5gen/R/MBAHx3DQAmaRQA+yUkACS7MwBjcz4At6Q3AEdLHgApGQUAuAn1/xx05f9O9ej/mcP9/02A+P9TXREA3SUnAHAZLAAm7DAAtqoqANUoGgCWkgcABssHADb8AgCxggAALmAbAPpROQBAFUgAU1pFAA2WQwBGQjQA7do3AEXySAC6DUQARtJPAPOGUgDh8k8AFx1KAMrASQBjiVIAW/xkAFfycQCMHIQAYoaSAL9klwAga4EAQbxuAOx2YQAWmF0ASvVPAHbRUgCRTlsA2xdgAClhfQCw1HoA1f2FAE3HdADwu2kAHAZSAOtNRgDHazgAf240ANytQgCWrk4AZjJpAOv7fAClv3AAF9VeADpCWgD9bDwAO1A6AEq+LADhjDYAFq47AKG1UABp/lgARRJVANnkWwARLksAyVE9AEQcLgAnbiQAQ6MkAPXBKgBaRiMA1TgqAEh6IQD7LyoA05IoANp8GwAzaCYA5KwoAH+2MQCP3TIAyJw3AJrRMgDyZScA1P0jAMCOIQD7iCgAwGAwAICxNQDhlkUA57tQACGbSwCoLEoAcARAADe5LgCSehoAYEUHACNy/P9pGwYAipsRAB45GAChhhoAFGEpACEaKQBvsyEADuYaAI1z//87Kf3//Bj5/wQxAgD3uBAALiknAGNoMwB3yC4AJXcZADBDCACs8xAAkTMDAKOtAwCmpwUAn2kLAELYKQAqnikAVb0oACLmKgAaORwAsHgMALuR//9mqfH/C+P+/6bTHACVvCUAWnM8AIFgPQASq0AAGq5AACqBNwDVayoAiFAYALkpJAApmRoAHzUWAMweJwCLKS8ARzhEAOPuSwCBN1QAxMpXACo9TwDBXE4AK4VDAG+8MwCDwS4AdQY6AH5xOQAkzkkAK7lQANYsVQBAbFkARntQANfyPQDOEisAim07ACTuOgDMAisA7OEuAIoZOABsv0IA3OlNACmnRQAuTEEAcA5CAFVBTgC2o0UAauA5AH6cPgD6JjcAfs9EALZ5TABQAD0A8S4xAB2rPADhw0EA+U8+AKcUSwATIVUAdDZbAA0CagDvh2sAcwRNAJsQLgDRlx4A3yoWABr/GgCgaCUAPak0AAr+PQCXTz4A+jEtALTyFgCH0AsAQFoFAM9m9f/nePL/XwgBAH3F7/9V2eD/bl3o/9+86P/s49n/bPbW/3uh3/+FgeX/vc7W/3aR1/+tGMf/iGWq/7aQpP/pHJv/MG6Y/3lUmP+2N6L/GxOi/95mqf/zZaD/Zd2O/0FUg/8FPHr/rNx3/zESY/9G4GD/oSlp/x96Zf86gWn/KEZe/90VVP+nPlv/z/5Z/+smSf+ynEf/nx5O/4cbVP/pkFr/sjZV/1hLVP/VrUj/3MtK/8+KV//aZVT/eBlQ/5QpTv90dFT/qVlZ/+eNT/9fUlH/h5dN/282Qv9rezX//dIu/16rO//XeD7/Y0BM/1ojTv8F0UT/D+BC/1kwMf+2Dyz/N9Mj/0/sIf+Fuyb/3gY2/2ZyN/+ZFTr/LwRE/2KfPf9ZYkL/B088//nQM/+XAzf/0h01/yVBKv/51Sb/dqka/3qfMf+2jz7/rhI6/yXxOf/Edir/q2gf/5H4IP8kCRj/FbYf/9knJ//SkCj/KMpC/w5LNv/Pazb/BzEw/yMaJf9QoCP/TqUV/26tEv/sahf/X/og/2g4K//fnSj/KKMq/7a/M//wsjH/RkQe/3NpFP8vcxn/HMwg/1mNMP925DD/aZU4/3z/Tf/BZ1v/XQRL/yHUMf9bhTD/ms5G/+ZOQf8GZzz/X0o//913Pv/fAFv/s45i/9IXZv8nI2//YMpv/+RCe/8w7YX/ioF2/xpVZv8AR33/DFCO/wlwjv+3h5f/rN6N/6XNmP/80pj/7saM/5vxiv+Azpf/i9qu/4i1uv8Uzrr/ETa2/+b1n/+zX6D/TJWx/00HpP9096T/eKag/wPxw//RFdj/PB3j/z1N3P/9NeD/xLTz/1zg6/8+/NX/SYnU/0X96v+Riv//MigbAHbCEQCWNhMAmLEaAFyvJADS7CIA7nEcAKobGwCaFCoADWQvAHerOACu+DYATSgwAH2ZQgA/TUUArDYwAOzWLAA0Ej4A//xDAPjKUADSQlUA1PtcAN+9YgCxD2MAyxNjAK/obAB5w10ABTBmAGhBcQDw3m4Azpx6AAlkeQCq2n4AWS54AF33cgByVXkANh2IANe2iADNB5gAWDWkAGjgoQD9GqEAN7GWAK9BnAAKT5wA+k2aAB5SnwBnMqwACLq7AAaMvwD2CMcAsi7JAD3AzwC7DNMAu7PRANuv1QDV8c4ABsjMAOjKzwAaq9sAmaLdAJ1P3gDcC+YAujPnAPPb2gBkVNAASfLGAJGCxAAOkdwAmvDYAElL5ACToOAAgAncAEoP2QDWBbgACVmzABt7vgDLNtsAsV/kAOcm5AAHteQA8P7lAO7A2AApd9UAH2rLAHfoxgDOsM0A4a7bAC+z7QCAUvcAXcQGAVCE8gBYBwYBRm/8AMjW2QA7wMgAT+WzAM48vgDDtswAvWLYAJBO8QAMqO8ARjnvAOei6QArG8UAhWWyAAwBnQB5LJoA3iWbAObdqAAEm6cA2QmjAE4VrQATFZsAlyN7AM1IfwDDRZEAfaKdAH9WqADpM4wAmmR7AMUgaAAa0FoA/3FkALSXaAC0m3UARNV8ALoadQCtR3AAHKxfACU1YADkJF4AL95UADH4WQBjeVIA6fFgAHXNWAAo40UAZ+I8AEdwGAAA5QcA/Nv6/5sT7/8IEw0ALgcnAKUJPABo7kcANTRAAIWnLwBqKAoADSzw/07V8P+HpAMAkZcgAGg5QgCnFUwAmWNLAG9PKQAzmwMAiRkDABUB9v8RbOv/uif//3Q2FgBCATMAcfQtAAtSGwDctQ8AXn7w/w6P6P89Ddf/GkHg/3vv9P9hAxsAL0MpAB7rJgBAlygAx54dAAX9HABfU/z/2PTb/6z60P/cuNn/f774/1EgGwCEjCkAKRsyAKrcNQC51h4AnEwEAEsB4v9/T8X/lxDe/3fK9//WQ/D/NOPk/3pL7v8Ofe//Qs31/zR05P+FXdz/Fwfv/30R7//xkfT/P/bq/4Cp2f+I0dL//8zR/95J1v9/C8j//J/L/8Jr3v96BPL/+s37/x2w6v8/e/j/tx/l/0AP5v+37d//rvXP/6JR7/+b6+//ZtsBAOIiBgCvYv7/Apv//xV28v92Wfj/VVvw/8ZD6P9szfD/EMDv/xtE8//sWfP/6Kzw/5OR7v9m1PL/0db7/xxd9v/V9QkAtWwTAIJGCwBQixYAERoMAOLN/v81vAgAj+cRAFDqDgBg/RIAM90HAJEXBQBR4v3/Wabw/8T0/v+lOv3/2b0EAG/VAQBB2PX/gWfr/y9r3P+0sub/P+bm/9bO4/8DNuv/2Zrb/+NN3/+6ceD/YjLO/3EU0v/QWbv/v/Su/6tApv+TRZ7/YCaY/2ofmP+cGrH/RV2//+v9yP8uUtT/dPHc/yPv3/+EUdf/NcLC/3I9tf/HwKX/ZvCm/5fdp/+4L7f/+8jO/9Tx4f/s4ez/IJ7z/6L5AACs+OX/PoXj/01d2v+IEMj/iV2+/4RusP90xLv/IjfG/3ca1P+lauv/HUT//yHGAgBGBAcAzhn2/yHN2v8vuLj/Cpe4/+VJvv8qP7T/793F/5400P/g6OH/avv5/yaI+P9+5v7/r0f6/xYt4P+Xddn/Qs7H/3ntxP99b8H/cLrJ/+DD3P+jcub/Z0b1/yjf5/9iVN3/MOvS/wJZuf+liK7/Dwaq/45Ytf+drcT/C+3U/6it4v8Ww8n/Q/HP/8e63P8hY9L/lQDc/3YTwf/iAMT/BcvX/7FMzP+MTMD/lsO+/8Yzu/+BWK3/WGOj/19vov+E26n/2MKs//kHvf/a0bD/wjai/z6Om/9Uuqz/XDiy/7rtrf8Koaj/fNiU/z6Emv8pt5z/+LCQ/4yakP8DKKL/nru3/9KNrv+2tKH/KK+r/0NNp//Rz6P/YiuA/2OPjP/5OZj/lUmg/7zJmv8/RZH/NbCo/6E8mP/FM5X/gDKK//rPgf+sJnj/A+Z3/y/Ra/9IknD/phB6/+3dfP+mlY7/4bSA/x2DfP+saYD/AKOJ/6zqgf+PnWz/htN1/2Qdf/+nmHv/bE54/9fNhP9HjIj/CHeW/4galv+ZHIH/Bw11/x7iW/9ly2b/jFZg/1IzZ/8zY3n/rxCJ/8Jymv8N5ab/u4av/01Onv9YPaj/hjuW/1UVgP/gOZP/0zqs/2bFqf9Jiqn/++io/1Rerf9AHbT/0Eig/9P/jf9Ggpf/mUSq/5Slp/89Bq//wqef/4bfo/8qk6P/rySK/xHPfv+WqYX/IseW/3RTl//uPJf/C1yL/9ZUkv+Zc5r/rEum/wiLsf/cb6T/kAuY/3U5pP90xZ3/BYig/+XWp/8TM8L/xN/q/7YN2//TUNL/ddS//wULz//Ln9P/QCbf/5ZD3P/Bdeb/sDcDAOBv+/8/qwcAtLsEACBaEABKS////UEAADiKEQCd4wgAi3cLAKb5FACueBcAPtkWABwnEwBiYysAYpY6AH1jOQDoDD0A//4lAAT5IwCERSkAmxoTACVdEQD5CyMAoAEfAPo7OgD36UsAUDM/ANejSgDPZEEAPL83AOTQHgB6zwoAb/YNANkQGQDpDT0AQLo3AGDHQAAbjVQAV6hXAGv4YwCAjW4AKMlhAMcMYAAdj2AAjQBPANWLSQC3S0MAgh9NAAuxWgANMmYAScJzAOSQfQCxK4EAEs59ALz3fgDiwHAAbcVTAO0wUwCpM08Af1JSAC8LXACpk2kAH/2BAIGSkgD6UKMAiI6qAI3yiACtF24A/otwADpnYQDkqFkAtIhVAEdnYQAbLm4A/S6GAOMYnwCCjqIAywGYAGDUjQCCpnkAiXxbAGiKPQDdPUgAeO9nANyjYABLeYEABCKIAMkZhQA7mnoAo0tjAOpZSwA2ZTAASUofAHYdJQDNsS0AXA9KAARqZgAGoWMAUY11AJpEbgBMAncAoeBUABcraQCfe2QAX5pRAABCWwBSWlYApxhWAIPuTwA5FWAAXnhSAEaWaAD1yGYAz1d5ADOVcgD1pWsAQsVuAEQ+cgA9X2UAxjldAM5TawCrwVQAOjBeAMAZUgBvJF0A5KhPANJuUAAcXFYAqutiAKigaQAQjEgANV9CAPGBSgCTHVMAWqpNAJkqWQC2TVkAwHxWAKfaOwDeTD0A0fpGAPh7QgCoVVIAB3FVAGapXgAs/1wAV8ZRAEfpWgBn3EoAwppHAL3sPwA7rjIA4NozAEtvKgCxBjcAoShMAFqrPgAFky8Ac6pAAP41KgCXIzIAD9IWADyxAADP8hYAY/kAAMRECgBYxgkAT2cMAJ91EwA6tfX/bT/3/xZr+v9PQvz/n8wDAGqcAgA5ThgAou8nAAnqNQDafDoAwUBBAIcCSgAMqUMASf84APJKMwAB6isATkMiAKk6PwDL0EwAfaBfABsFaQDNUmEAyR1eAEwKWABs8FUAmRFGAFsVQwBZ8zwAUf1LAEVlTAAZblcAuRtvAMyzawCd/3EADbJpAFUSSADCDToA/tk2AFBEKgDOcyoAKQ8aALvVGABBZCoAGOksAH+YNAA/WCcAOmIhAHI5HACIJw0AWpb7/1ELCAB7Hf//Nibw/0gjEABsABwArHArANhtGwDB7RwA20oKADl/4P8tpt3/dj7h/1aX8f9+4/v/ajH3/+nZ//+L3QUAxYcBADU9/P87eOX/aYvl/40r+f9ES/b/zjn5/3r3CgDabf7/qCP+//riAwCBovr/6bsBAHKiBwBaQgEA67rw/wkV2f9lmtL/RwjL/4gQ0/+loOL/aP3Z/+9t+//JWQgAuKHy/0LP6P/modj/g4HJ/8E1v//Uur7/ECrA/8zuyv8NItz/Czny/2FA8P81HvD/RyPk/4Why/83+s7/b4/S/48W0f8mF8v/+y/Y/zLZ2P85uNr/nUbb/zLTyf+uV8//K6zA/2ZVvv9LrMX/0Zi4/6sLyv8+TcP/0hzI//Dr0f9rdcn/TvPD/6yAw/+Wprz/NFux/z8ZtP+sj73/wcO+/9sJuv+bVc//conT//ybzP8HCMf/JMvT//6Lvv/4ub7/H+i2/y7Ar/9/p7//oAG3/8rOtP9766z/7G3N/7Hqyv859sf/QHrO/8+u4P++0+j/vJ7K/7ijyv9Dlrv/kcy4/0ipvv8tSr3/g5TP/0iTxf+m9sX/wM3J/0q02P95zN7/3qff/xisAwA4KQgAXFUDAKqU+v/iLAEAJhXy/9VP4//0Kdr/LXrm/6ol6f9TdOr/BQTs/1ME2P+13/H/l7/p/5uC8P9a6+n/Iu/u/5ta3v9DSMz/bd7P/3nuyv8ydNj/FfPD/7mN2f9Zi+L/niba/7BJzf/Rs8L/+CPL/6a6wP86aMT/bN7D/2S7vv8fgcX/g4y//4XD0/8Svd//PzHg/4nF6P9HfO7/ryvZ/0kbx/9hULr/D8yj/0PxrP+p67j/6NrM/8xa0P+C2d//RAfa/3F3zP/Exs7/i/HG/6/jx/9Vj8X/nnXS/8321/+cSdH/YR7Q/+vMyf+/Xsj/djm1/4+wsP8U8a7/E0W5/75QxP9gEbj/mfe+/+wqvf+gJL//be/A/0lgu/8vebj/G4ef/9CGnP8JM7D/sDCn/ywerP8Lsbn//0my/2WeqP8RlLX/V0Km/1hZo/+YKZz//aic/7Cep/8uspn/bfay/6eJvf+He8P/1TfS/3E/yf9uAM3/+p3T/1P0xP8qEbb/Jby6/3nRvv9Gb7//mMbJ/6Q90f9wKuH/zK3i/6TR3/+PE9f/S2fN/6wr1/83K8P/9xTN/zH45P/0ldj/G/jj/3O57f/bRe3/2Gnw/097/P/0yeT/QoL0/zTz3/+6oN//vr3o/yiQw/8jFeb/i9HE/0LOxv8/f8H/yMzA/xcY2/+GNc7/laTl/4e03v8ZB+T/vSrT/zfOwv/Om8b/T1Sx/yh+wP9kaab/iM2v/9r1sP/2X7z/Vx7w/8AKBQAZnvn/EhbU/zSyyv8lkOL/BrIPAJql0v+DgKD/SQKp/5qN2P9ZbxYA47L6/+2D2f9whuj/I7vi/6jTBQBStgUA4zP5/+R3+//QINj/5WHU/8ytyv8F3ff/D58TALDyFwC7LxMAVicjABzAKwDDthsAKgkSAJfe9v9e0+//2Szq/6oQ7/9NGPn/H8MRAH2LCAC8NRIAuXkbAAVpGgDsODkA9sY3AJInJwDXxvX/kpbw/wox/f/8cg4AtWIkAB9NIwAvLTgAJkk+AJoxNwDhSUIAZtUxAMLZIwCzFiEA3iYQAHPnJQA/tCMATFk/AGnSUwDnBUQANbdHAAS6OAC6IjkAo+QkAAUzHABz4kwAfkFPAM3kVgD5vFwAc/9fADNYXQAVfl0AXihFAM71JgBECDIAvYA4ANZHYgDyV2QAZjh0AP9kdwCvcmYA2GZcAGVWYgC6c18AVx9JAL8fTgD+A18Ax5RnAIVgaQCji1cAXfxfAMLeaADjvWIAB65uAJ2wZwCBP3IAMddsAK0rYAAorlUAA0FUALe2WQDWh1gAnN1IAJ4OSgAK1jYAqBc/AIQyTwD/8VgArN9kAKMsWQAGCG0AkypdAB7VWwDB40cAFigwAF+6LABR5hYA3GouAOqJNgB9Vz8AN2pYAG2YTwCp6lQAChVGAKi/SgBt9kMAB9oqADxWNwDjIykA5sInAA0cGQBhyyIAj9cwADiGMgC0OjsAxg1JAIrWSADI9y8AuMo+AIvwNAC44Q4Ah1YGAG4Q//8IUfb/BssIAI+YDAA8MhYAzIwmAC68HACn0hkAQ/UZAEnbAACAu/X/v8rn/1cO4P9FE+j/xYvf//PT9v+HtQcAjkb7/9aeDgDFdxAAznUPAIL6DgCxIQgAxjL//7vf9v/EVv7/0+z2/9IoAQDz8fb/z7MCAL/p+P/tTvL/P7Dx/9ON1f+uFc3/fS3K/y4I1P8h1s//qdTx/wYz9v/J6/X/3hT+/1GI8///6ff/t37p/xOg9/+e7P//OV/x/5rH9f8YXAcA7rwQANMDGgAPYiEA+XYWAPbfGgAcmRQAiA4UAEU5GADjVBYAts8YAI+6GQDq4SsA0RohAOWdMAChoTYAA8pHAHJmUgDIx0UAqChSAGwYQwBfl0sAM6o1AEs7LAA7QyEATh4SACuPBwCiL+r/QJDn/+Un6f+89Nv/21Ph/+/n8P8nuun/ZMPX/2+30f9EGtP/gArN/9V+2v/tiL7/JFXT/5spzf8fHcD/IM3K/7jBu/+GFMf/wPC5/025w/+JnLj/S+K1/5Cowf93zsD/13nI/w8ozf/zx8//JRfn/8gZ9P8Aovn/d4QFABMvCQD6nAgArAkFAKcdCgDtBA4Al9MJAC9dCwDr6AoAVJMTAJhqJgCXQyMAPoc8ANOoPwBTLjYASWsvADAJLABEZT0A7JNAAPALPgAIcDcA4LQ8APvkNABdrDkAIdo9AEicRAAB20YAU48+AAP3VgCM9kAAqzk4AAD7NACNchgAUjIdABdVHwCCIigA7YguABU5NQAvlT4As3tIADmWRgCq/TwAqkMxAEO4IADdJSYAE24WAPzuGAClfRcAZe8NAIryKgAQGx4AC+oRADwQ//9A/OT/IgTQ/4iWxP9VpMP/VyO+/9v0uP+W7q3/9Ja2/0vmq//7tqf/j5Sm/8qTnv/Xx47/S6eF/ydji/8yI4z/6Daa/6ISo//uBbD/6yu3/2Qw1f9dVuD/gRjj/9xl+/+OWPD/whoDAGY7CQCdjwQAt8cTAIZFGgDwbxwA9/csAGmANACoR0sAYnVnAEdnawCwf4UAbQuPAHLzmABg/KUAwFa2AD9AuwDcpLkAoeuwAKK9nQBlUZsAXlGSABsthgBF1nsAZ3hwAP9BcADPJXQAgBV2AJmrcQBrznAAiwx1ANn7aQDcKmAAqqhdAKp+RACed0oAo5pEAGNvOABMgioA+UUtAAfhMAA41SEABLMrAJqxDACZMgwAS0P6//w+4P8I+sL/XqjU/40Xyf95xsT/0SDq/xnk8f/Vbf3/IHUBAImxDgCFbgYAhmQLAKcE7v/pMub/Pq7j/9yW4/8Lo9n/asDW/znMy/++9LL/jK+2/zeUmf8oJKP/niy0/6morf+veL//0w+9/7tfzf9LDNT/2grR/0Yq4f+D+t3/ogrZ/xT9v/8OS7f/Ee+m/+H2q/+WeLP/MdbA/4+zwf/xIsr/9gPV/xEtyf+N49X/4Ge8/xaWu/++Daz/QR+r/xRQm/+3xpP/mDeb/wqll/+2wJj/mNqE/17Tjf/quZL/Mv+O/7D9hP/rOn7/CYd+/yLIjf/G2Jj/9Wes//OSp/+ohbH/2jq2/+BOyf/6OcH/H/nE/wUb2/8+J9H/R4PZ/4/P1f9Sutf/Rizf/3Tu3v+sHMv/ZDDS/+PPxP9T/dH/dT3I/zkM3v+eA+H/9rnL/34jzf8/jLv/LYS8/4Ssrv86lrj/afC0/197wv/tCsT/y4nJ/3XI0/+oBLf/rcOm/2BPmP8Ztoj/esSJ/6Fyjv8QcJX/bmuV/1tSlP+wjpP/vMKN/7/Nmv89c47/RxOW/ySrtP8OkLz/xm7M/8znvP+kMcD/uFKy/0FWqv/mA7T/Jsqx/+Przf8yAd//vKXs//u19/+SZAYA+58KAA1jBwCFTvj/kXLe/6610f++6Mf/YOnM/zwOy/8FWNT/XZPp/0EB8P/BPgUA41P2/+RP8f/TpvL/lV7g//951//YJt3/D/7c/7Lv4v/lAfH/9pzw/93qAgAvD/D/nynq/zp57f+BhOz/QAr1/1qR5v8MFuX/xwvv/2Zv9v+YkPP/onfq/89R7f/Bb+b/vxTf/3NX2v/8v+L/Aq7x/6dlAADvbQUAbPIFAGnd//8Kvf7/pPTn/76i3P/KT+X/gpPO/+hb4f+7wcz/EIrQ/85l2/8QH9T/TfbP/4Ar0v9Y69j/YcrJ/0L9yv+4v77/Gse4/83Gtv/tM6//Ngqv/7tFpf87MKH/euK1/4T/rv/CYqX/BhCz/7lDyP9BQsn/U7Dc/x0J4P/2q+z/xZvl/zrE2f9feuD/CsjR/ys24/+2Sej/uq/r/w0T8P8uywgAULwLADQtFgCa4R4AQ8gkADGZPQCgEj0ABnBBAF/uQACFRDcAT4M8AMDwNgAV5TwAgjBHACRvVAACOFoAF49WAEIBbwCFUG8ARw1xAFSpYgDVG1gAMsFDALBTRgBdTjUAZ4MrAMh3NADDchMAcbEuAPEONgBk0TAA9IBKACkEPwBv8joAHTJHALHdOgA3ATEAZy4zANZUIwBX4hcA32whADLmDwDp7igAwl8XAHZMDgD8piEAlzAaAKN2MAChrSgAER4jACeoKgDkCSMA+z8lANLEIwBXnCEAELYnANciIAA+Kh0AnYgcAL7FFwCpQggAzFcWALJGCQBXz/z/H04UAAeiBwAZbgUAAScoAJA3GgA3DiIAdg8mAP5rEwDMQiUAhpsqAODIMwDYqSkABeApAH7SNwCsZSoALTEdAGgGFQAckhMA0b4LAEYiEwCK9xEAfRcOAA9vHgA/HBoAly4nAFDsIQD3DCAAR4ErAMxPMQAObiUA+fMtAM+kJQBulS4AGqRKAOOlWgBa91UAZC9SAHcvQACoIjoAmHFPAEi3NABglEEAIQ9IAAWDSgDuCk0AIo5RAKg7XwCvZV8AMtxTAPi7YQCWOlIA+QFZAKFRXACA/lUAAnpSAGxYTAAM/VwAS9VTAMz5bgARRngAOG5tAKIBfgDQ7G0AqXVjAC6AdAD2PV8AsCBbAKu6YQBxNmAALGhhALxIbgD2j1kA32JaAKlCYQAU6kcABT1JAJ11RQDrZUYAFR4wABUmLwCf+SMAS7kRAMyHJAAA/xMAYQMbAJWsGQDXvhsAD80sAJ3LLgC2/TMAo58sAFoFJgD4TikAkJEwAMMCIAB93R8A1OMiAIJnIQCvQC8AFPQiAOweGwArrSwAzSIhAHV3GwCFHScAelshAB9jHAApURUAbCobAIvYIgCcCCQAb7oiAF4vDwA70REAAaYLAHmVDABOIwEAPKkFAG+MBAA2g+r/qePr/3C/5f/Fcez/MV/q/3+B4f9gs+z/8eb1/0Ou7f8rq+7/Va70/3lxAgASBPf/jsr7/3Dm4P++mOf/78vX/wBDy/8XEcb/q2u7/6NDz/9d/sX/VF/n/0fu2f8BlPX/eOfS/6K9yP/57NL/kwm2/x5jxv8HV7X/dim2/8M9vv82x7H/y63A/+S6u//di73/rdi6/zt4vf99ML7/8Dy7/+sPz/9c5sP/dCrj/9sAwf+KgMj/r0vJ/xdzzv9QPM//kd7F/+IR1P8dmM3/siLl/w0m8v8legEAKJvl/4NxAACIWvX/+Gvk/6nx7P+YVO3/00r4/0wA8//cFQIApv31/2vd/P+yewsAsqgEAPZwEQAvVwkALQUBAAiB/f87APT/xfwBAG478/+17vH/JOvr/zyj2P9NW9b/73fc/1Z93P8Oz9//lszk/3d31v9M3u3/gA7r/9zT7f/7jvn/WDDb/3YS7v9JM+n/U47s/0K5//9FmvH/4BT4/1HU8P9uUPL/LVb+/6YyBwC3qBUA4NsHAEznBwAV6QUADen0/+ofGQDAJ/n/Psn6/4S0+v96cfX/AXkNANyO/v9tPhIAgsMLAC+7DgBJfCMAB5QfAIuJDwAoJBoAH578/1C78v8cMfr/9FTv/yVw8P9K+gIAjikBAHWiBAB1ihAA/sETAPEdDwDCPwsAQG0AAHpN8/86RAMAwlz4/+Lm+/99Jfb/I+AAAOdiAgD8zREAfpX8/2Lw/P8VUgwAxj8DAL26AwAEEAgAkb4LADcs8//cmO//1tLm/x3d5v+OJt3/VhvU/09V0P+kANr/n9jX/y5u3f/jy9v/zfnk/xyW7f+fTeD/XH3a/wY01v8lQtH/92TX/40L0P9zJs//uiPV/wMi2P+Pi+X/Bavs/yLdAQDDhfn/RlD+/9s3+v/1MgMAXKzv/yVt7P9mQvr/xxXo/xC49P8hxvD/s7v5/3zc/f9Yjff/udsCAD/YDAB2FxUAWy0SANxlBADvrhAAjDn8/+UMDQA8VBYAgeoSAJPdIAD11gYAkPkWALUpFQBPXxsAyCsUALx2BgAYQwMAZjn8/wW8/f8YcggA6RYRAAVYBwCcfP//pHEQAN4UAgButQgAWc4HANaWAgBeHxkANc8EAEQWGgAVpRIA6Z4UAPYOBAA3UwcAXFUDAJMVAwCJnAAAojkGAAYEDwDong0A3E0ZACFXBQCYSvr/Scr3/2Px9P/CFOb/2tny/92K4P+7ot7/BMzY/7311//b8dr/rk7M/yBmx//oyc7/qSK3/5DNx//WnM3/AXPO/8KYzP8xX87/9yjK/0OVuP/fjsH/Lq2z/99Hu/9CeLD/aq/D/wtB1f8LE+v/nGDe/wZc7P8ZLO//QrX7/9vR9P8cNNz/ju7i/3SM0/99neD/KCTk//yJ8P86I+j/N970/zQU7//32fT/sW/2/wD38v/jNOj/XGvm/0S+4/8/1Of/NVL5/4Gm2v8a/e//BDXT/23I1/9aTtz/9t7X/ycx4v9g0tz/oZfo/2nC2/9M7OP/dJfh/2QR7v8/Pt3/34fY//eZ0P/swsz/P6TV/zkBwP8c+tv/kpLb/wdvzv/4DNv/6lrM/8Gqy//20tf/MKDU/71ay/+LWtT/gO7M/80e0f+necr/o/PS/zcX1P//Kdj/j/Dw/4Py5v8g5Pz/H9rv/5dj7/+yTeX/i6zn//3o2P92EOX/v5vm/6hB4v+PEPz/qg/9/0BAEwBqpgoA2UQRAJX0AAAhcAoApHf//wY7AQC2FP3/aHL2/+Az/P+kiwAAApEKAHnWAAADNAwA4OICAM+QDgDJZBAAg4wNANL0JQCwoCwAFMYqAIhiLgAOcTUAskoyAM95KQCgHjAAvLMaAPd3IQAvtB4AS1MdAKn0JgCb3iEAmygpAArUIQAjAzYADNYoANUhHgDG0hwAHtQaAArXLAAxGSsAIbcnANpyIQBPsiQAjnYWADYQBwDoMwYA6AAKAEIZCgBdOAgAb08OALG9EgCarhkAoAkkAKkDKwCmGSAAgTESAPvqHACPyBcAzE4TAN/HGADETBkAFLUWAPYSCADRdAIAbPcBAJQ1DABj9/3/TI0DAKYeCgCxJgQAHOoKAC69CQDu8BwAfPQWAK1eBgDJTRAAm10KAL6VIwCSViAAo/UsAG7SJwDNqxwA4X4cAHF8EAAwwCgATSMmAAN1HwDVSw0ABcEkAAE+HQC/GR4AibEnAJwNIQAF3hMALsYnADWsGwAO/hsAiJklAEkmGACN1SQA1GkYAHQAKgDOOA4AvTIgADKNEwBwgPr/IhYPANPk+/+QMvv/vP///6vUAwDmEAkAje8PAIeUGAAYxRYA1RoUADgsDwBb2gUALmwEAExMBABVYPP/88/8/4qMBwAttwIAtWcRAI1rFAARVxQAZXkQAHqXFQAMNBgA594YAFnHIwAReycAfEQfANZsGwCOJxsA/jMWAHM8JAADMRgArnYPAFFdFgDkFw0AA0wZAJlqFABFHBwA69AaAGoNHACiuQ0A9IYJANx3GACOlQUAj3saAL3NDABRFhoAgAcQAChHIAAoORsA3aAOAN/DKQCa3xYAOPslAKXHJgAWpyoADiMvAAPeOgDz/SEAMkQlAJnrKwDD3CsA0BsdAO8XJABdoyQAGD8bACu8LgAC3CIAYwchADxuIgAOCiQABlQdAJdXLADReysAyugdAGWTFQD3iwoAKjwRAOB0CwCfiQcAr8kLABhxBwCvcBEAg+z3/7sGDwCANfz/KPTz/6oa+v9oivb/lQoGAGOZ4f8KSvD/59zh/7DJ0/9VeOv/ubDs/08V5/961vb/cmX2/4/G/v+3NPr/uTkCAJeaAgB0tgYAiZUXAJ9NAwA9vRIAk/oMAKypCgBnJAcA3EIFAAA/FwCuDhYAtFQgAETbKgBnEh8AqAMhAGpODgA4fQ0Ai5YQAPmP5P9dQgcAeGn5/+4yBABUOg4AuVT8/+7wBgBdnO7/U474/+sC7v8Ec+H/wuzU/ySE2/8iw8n/jnLA/xXDr/97BL3/ruvE/x/ovv9gN8v/YNir/8Wmzf+tdrr/6e64/yKcyP/fkcD/6+m3/16RwP/+5bz/0sW0/y/nzf/mBcz/NAzd/1VG2/+1+PL/0p7p/zV47f8vQu7/Odfv/+Xw4f8TJNn/rZny/6Zc/v9yqwAAXPwDAClHAABnm/v/4VEMAMlqBQC6SBEAA0wWAPJ7GwA3thsAUlcdACWgHgDpsikAtfEaABTdLgBD+i4As20oACaHRQBPBiQAotgrAK1FKAA1whkARSknAEEFIADrOBQAgUgDAA3UCADBkQEAuvMOANz1DwC53wkAeZAPANERFgA9cxkA7PIcAOCZHABt7B8A+UgyAJTbDwAtfw4AFtcBAAW39P/imOv/fPby/zaV+f/j7fT/4hIFAIHKCgCcxv3/WGr5/4ov+f+znvb/d1MIAPq+9/9mHf7/nIXu/wED7P+hae//DX7u//ua8/+vxef/9DAIAA5Z9f9u8gUAxdH+/zCb6P+XePb/F2bu/2dxBwDc2wAAinv0//sK8f/ewwEAqeD1/3hrCgBjIhUAqTcDAHtQDgDZ0fT/QUzw/54c8P8UFv3/xBfo/+uF7v8Yeu7/+Jvb/z1z5f8229n/e9Hd/58Q4//Mpff/wrHu/9k38f+ls+v/HbLx/yQp8f+LqeH/qqz5/0kp6/9whdb/C2ri/8pS1f+dRs7/xlHd/21T3P/+R+r/IKr0/6hO9v8UZv//yWP8/xFz9f9rO/v/4Ob1/yRv8v/A8/X/37/j/wEByP+8deH/jDfY/zLl1/+q4PH/cqDx/5227v/Le+j/LfDZ/3GKxf+1Mt7/t5HU/7tH2/+Lg93/D83O/9Vt0P8TVN7/xknb/6803v81VOD/divh/ycF4P9MN8//3Gfb/0zi3v+Qw+r/OWHo/wbV6P9Rhej/9Vz6/0zB6/9sQwQAs5z0/0CECwD+lhIAKjXx/zu7DQDVN+z/Hn38//VJ7/82/vD/Dofp/6at8/+IEO7/2STl/59F7P9WXeH/S8zy/+kf9v+TZ/b/IHz2/5iA9P/5Ber/mADz//+a9f9bkvn//uDu/5YU7/8F+e//G7/r/zEi8v+vJuf/c2Pm/whe+P/XBvD/D1f8/508AAAmFvv/u14DABJr8v/mVvj/wpkEAPZUBACj7gYAKDv//74y7/8jJPT/vpzq/0DZ7//gCf3/aqH8/1AD9/+z8fb/QNH2/wOJ7P+hdPv/ePQIAPgZ+f9cIQMAffD6/3k4BgC+4A0A6UMKAF4VAgBu6AYAN98SADLK+v8qQQAAL5Hi/74c+v83evP/HEv5/3Pu9P82o+P/ka75/7ZJ8/9QEfn/I9L4/2fVCAAs0wwAu579/yms7P8b7QQAHIL8/4up+/+uhff/U9fs/53s+/8Elu7/xXMBACzGCAC1Xg0AfC75/y7hAgD24v3/L/v2/0mjCwCqs/P/66f5/0MKBAA9NAgAeZMLAIWcIQDGghIAm0MSAJ+QGABEAhQAe/gqAEWMPQAawzcAGo5CAAcgKABq8hQAWF0tADfRIACYAjMAUAAzAF9UJwAWBykA400uAB+9MQDhTjIANZozAMjLOQBtW0AA+AU9AJr7NwBLXysAxMkuAC/lOwBzBiIAwoAhAAq0JABvnSgAw5QWAJd2GAAR3yUA4+UfAHEaNACH3h4A3mcsAEyEGQCryhkA614kAAdjHABi2ygAVJApAPP4MgDHxTMAqvA2ALIxLgDtBCoAp1kxAOkgLgCwSSMA4uYtAJhuPADnLjcAC/gyAO0JRgDoDDUAfBk8AISDNwCmzDsACIxJACXUNgCc+DcA4m4yAI10MwBesScA8MY6ACCQNgBZXzkADlwjALX7KgDkaSsA69UgACTsIQABdgcAQ3kdACTfCgCRVhIAH6T5/994+v+rUwgACmH+/zP7FwCbSfv/jrn5/zbH+f+P/OX//Jzq/7pF5v8kLez/ec3e/5+03/9CAtb/QfbT/yfs1/+hQdT/ZPnR/9PLvP/H3r//NknB/0FJxf/TP8r/yMXE/4BP0f9dZdT/DtDS/1F12f/JqNP/6N3b/944yP/5ucL/TdO//93Ts//bvqz/hiu//63ttP9cKcn/DtK//zoOv/9mNNT/P6XH/18y4f8zJNX/oiXl/8JS0/8AfeP/sxTF/3EG3v+uRvH/LpzK/+g+6P+T/c//TWTV/9id0P9nguT/7UXo/6Po5P+dy9n/RtDf/1/Az//MQOD/5Ofa/xrqzP8d+en/SlS0//Hy3f+PCdL/iufT/1DW6P8SE9z/FZrT/3O83/+3k9j/tVLL/5B65v9l2Nz/q/Ll/2K54f9fLN7/Rifx/1tv2/9p+9j/7ero/7Zz2v8xu+H/jWHR/x463P9h8dT/VMLg/1+M4f9GL9T/DEve/8IB4P8rCe3/v/vi/4P87P+YOvf/xOfu/0aS/v+X5/7/ZwL6/54bAQAAKvr/HuP9/zPI7v9JaPf/VlX5/4JnBAAyNAkA3D/5//nb8v/Wtv//yfkDAAyG8v9WXAIAcakJANrJFwDxzBMA+cETAA8UEADURBkA/sgSAHfMGADl7R8AdJwfAOZVGwAU8hAACn0bANnrIgD/vxkAgc8VAAOvJgC8owwAt0cYAPON+/+0BQoAs873/8Qt5f82DPv/Zmvm/9XN7v+AGfP/azr1/1KZ+/8kEvX/19/r/5hL+v9SYub/jg30/1/B5/8CSt//Wazg/5I17v+bdOL/rO7d/xDU+/8nLeP/jmXr/zI7+P/FCvD/ODrf/8oE8/9z9vr/WbLq/4La9v8oi/D/KEXt/xW97/+oONb/gMDi/5Kd3f+dSev/ZDTt/6379v98pv7/3cf7/yAXFwDM5/r/YqMQAHhSDQAXHAwAQ40ZACTwCwCUjAIAl0oMAMudCQCuWB0AqRAUAHqCBgCA6BgA/ukQAI+bHQAilAwAYzgcAA4mFQAE4xEAEAYaAP6wJQCH8CEAIxcbAKuIKQCyihcArp4lAI1BLwDnEioA8cMhAAzfPQBymx0AHV8sAP7dLQAUcB8AgkxFAC57NQBFhVAA/Cc3APkGRAAl6TQAExY3AAtFQADlcCsA5k45AM5AKwA0GTIAYzA9ABqOPwAywTkANmEyANluOgALRD8AZ/QrAJZlPQCBmy8AyPQsABUHLgD+iiUAulAsAFslJgDD3CQAa7MgAPQ4MQCrTjEAnIssAOQmNwCMeiwAeJ0lAFHcMQBIKTAAcrIhAL/2MwBgkTAA0cMvAPAAOwBlbywAslYuAMpYIgDdmysAziIqAG3rJQB7JisAY8wtAEn/MACQuSoAlxQ0AHMjQgB8tzUA4X4/AE/5RgA7UjMA5MA/AN89OgCIsD0Axyk2AI3tKwCNwDgAknEtAAsOJgBKiCoA58wpANUrIwBgwBYAgpUlAPCjKgAdHBoAGEoyAO6eMgB5axIANNQlAAGbGQD/Lw0AA+wdAL7CDwDsLBcAz80lAK5VHwCDeSMAL6wkACu6JgDbSjgAHWYjAES1MwCqESUACyccAPPCMADWcCQAF84vAOROPADGjzEATBA5AG/wNQB6qS4Ai6g8ALC8LAAmFjEAX38tAHl2MgC7VzEAQOMpANuyKwAXtCQA6ZIwAMidHgArmBQAeP0fAEAkAQDTnQ0ACrYJAOcGAwD5h/X/d7bn/64l4f8rx+P/+w3i/4G63P8rl93/iHzN/y8c0/+Wkc3//LHI/6HbwP+rXND/FvC3/zsO0/9GN9r/HOrZ/yCl2f/VRN//8Vvk/5Ue4P/TW97/KR3Z/0M95P99HN3/D9vl//8S1P/ixvj/+czz/6lv7/86ge//9Y70/xqHAgBoxuv/qIz9/+0R+v/Qe/3/BCgCANZr/f/vGAcATtAAAHSWAQD0awwAt1YSABOw8f/ODQAAh/YAAACL8P+BeAUAbHsGAB5P9P+8mfH/zYns/z6W6v+Ki+//ZOPZ/5UN6/+9COj/RRLl/4F60f/9d9D/TEbJ/1WmvP8kRbT/YMq2/+uxs/8p8bD/v3e4/9Mmqf99MbP/hN2o/wdJt/95bq//v8Gm/xMSp//ws6L/bUKh/1appv/BWJj/PAmW//G/lP/LMIT/Pfab/53RlP9DQY3/VHaL/1LboP/a4Iv/ET+N/1dkpP9wo4v/ukSW/9Fuqv8RApn/ZGGc/7KIoP+Mepr/tsCi//TcnP9Gw5v/PSuV/2Zynf96Fpf/GVGo/3AWoP8u2Jv/3J2a/xvypP8FxLL/2k6p/z3Fvv/fqrf/D062/5Dnvf9RoLv/nU7I/9xbwv/czcr/jlXB/9XQx/8CZ9X/ubbE/2f8zf/Ezcf/l3bK/3cL0P+w+9z/XrLX/6Ch6P+cU+//Ghfj/9ft6f+pMt7/ac/a/11u9P8cauj/JoLi/9nf4f/Ezdr/Sb/h/7TM3f/PhOP/x4bj/5gI5//4nun/DwHr/xOd7P9DtvL/sAXq/6Bw7f/9Per/LyLp/73n+/+4rvL/x4b9/wvrBADqOv//PBALAJg6BADAsBQAUgcaAEopGAA8vC0ADd8kACluIQBDoyYAdssoAAqqMgARBjQAEdg1AIFeOgDpJz4A7tc3APhIQAAX4U0AqqRHAHlfSQBgL0wAc7g+AFZ2PADrv0QAaAc7AFaLRgCY8E0Abt5KAC/oXACYHlQA1VNSAGyzUADMgVAAtvdNAHd1QQD9B0sAhNpKAODyRAAd9EUA7mBCACTDPQBFpTsAR/ZFACOEPQAyT0IAIvdEALwNMgDEmDsAwzQ2AIdqOQC5vDIAW6cvAMmgJAD5wjAAK0w1AB8zJwDjcCMAgqAeACp8IwDJ1xsAuQYKADy0BgAGkhUAvdsBAH16DAD40v3/ZND9/8LqEgAw9P7/41oGAG7UCgC8ufj/gEkTAAEjMQBiBT0AJ+1ZAOhqQADXPyYAtCAqAPQAIwBDEBMAwMYhABscIABa/BEAFwARALzIDgB4xREAg5gcAGw/IADuMBUAagAMAOAvEwAQ4yIAgUMoAKjPKABHsh0AxmIcALQaCQAjIAYAHCj1/woy7f/BReX/ngPW/2ms0f8GFsz/TcHM/+a00f95O83/Oqq5//CzvP9gsLn/uovQ/x2k0f83Ut7/p4TQ/xio1v9Jp+b/pFbR/+Vn2P/4Ec//4Pnc/0tG2f9KGu//2fLq/9z55P8y3Ob/zHXw/9X4+f9iYff/SFsMAGA9EgB3kBAAVkoRAJLGIQAvEBQAWjQhAO/YEQB/iyEABGkvAGgFKwDT/zAA6vkwAOFvPwCLED8AIkNAAHHnMwBNREUA0pY9AO7ULQAaqjYAl9ooANuuIABxhR0AYRUJAN4vFgBv3B4AqlASAJiTCADEXh8AoWouAJVfMgB8STUA+xQxAGS8QwD+e0AABLxCAPFFRABeHUIAY44/AHt4QwALhDYARjgnAE+OHQAyLxcABqoMANs1DgCMMhQAnZwHAF9QCwD7iwIAj/IUAB/aFQBQMxQACzUeACWMGQCxOx8ASygmALbmHwBC2BYAH/sKAFYe///VagMAgYH2/7sV9P+JMuf/d27u//0V6v/OGuP/xVDm/6RR8f+yeQIASFLh/8tD9//0TOz/Ednr/4iy8f9f3fX/OTYEAA8o+f9YJfv/oiTu/wcm+f8qyu3/Rhz1/7875/8R/N3/KqTh/+ZS2f+MVtf/iGHQ/yiiyf854tL/PXfL/2J5yP//NMT/Z2LS/4f91/8sEtb/fpTi//c30//zG+r/YX3T/yzk0f/3CND/QO3U/zV81f9GkNL/CdbT/4it3P9Eotr/ssjQ/xuJ4v9IfND/YMjc/5IY7f/++db/iL7Z/5H22P+kUdP/4I7j/9kS3//zWeT/NBvz/79G7f8U0Nv/OtDd/2AX2v9tMtb/kbjM/wbK0f/Mmcf/zE/f/0w24/9al9T/ImzY/86o3f+lM+T/TI/K/yH71/+V3dr/hwzc/2033P8atdj/z3XT/7it1v+7Ndr/X9rU/9da5P+tAOL/iXjZ/6RF3v/Rgef/LNHi/y0N5f/tWfP/1xD3/7hJ7v9RRPP/MZwHAERN+/9WdwcAfjL8/x1p8f/D6v7/lTP3/yWJAABPYvb/JCb8/3PhCQANIPz/FSsCAIdFAAByT/X/Zg/0/xMn8//nXvj/Prr0/wbW6v+Hlvf/YWn8/yB9+f9X1/7/lY/1/1gPAgBUyhQAVhoBAML5CwBOFRAAd/AKAAeYGQByzgkAzYgYAP8YDwATjRkAergSAD18FADS/B4AqMgSAEJlJgD2KxYA7pEcAPJfHwAmJB8ALyEjABd2KgAa0zYAApcmAI5iNgDp5DAAwLUgAOdsPwAyjDsAoIk+AGLfNAA9QTQAtUw3AN9iLQAo4ysAFasbAMLqEADmLwsA0EAIADRa6f9uafT/VOnx/+NG6P9WCvj/g6Xp/4hC5P9QMfP/5EHe/wQ2yP8GWcT/Qoun/1u9oP+1TZP/Vm2Y/7cXhP98zpP/y2mI/+kSdP96Wob/2kNr/9ZYfv9cF3L/+yZ1/+/jgP/14oP/H5WN/4Mim/+ZzaX/6+qo/+/qvf9Lc7z//we9/z5az/9GfuH/ea/4/7VBBQDbsykAyOQ8AA7fQwBI8F0AsMxUANkxZwDRVlsAZT1kAFgNagDYu2kA77drAMBNVgBF0lkALAc5APw5OgBkbjQAAYcrAF1qKwBNXygA7h02AFzANADbITsA+Pk7ABnnTQBlvFwA6e1nADO9cwBbb3kAn5mfAJAvmwDK558AvI+gAG4FiQBQOZkAl+h5AF42UwCBKT0An18nAEqDEADyewoAhJPw/6/i8P9mfff/DPHj/4G16f+iH+f/tD7p/6xy3f/JvNj/MjHR/1nP4f/dqdz/q/7b/0Bz8P+KCtP/aLHZ/xSA1v8jKsH/XZjT/0S0wP+GcbT/2Vuw/2NprP/8MaP/g7eU/5irn/8eypX/S2iS/7BOq/8reLn/nB/N//ok5//EP/T/DJkUAJ8HLQBZojQAHkc/AAD3TgBgKF0A8HR1AL/ZfAARZHsAa7mKACuxjgCAqJMAFNOYALzBkQBN6o0AVTR4AN2RhwAbL4cA4DKGAAGzmAC7LZMA+jOYAN0GjgBXMqMAesqaAIjymQDr5JoAnQWJAIiiiADKSHYAwGF0AP2OUQCQWD4ARzI3AEiWNADD7ygAs5ApAJJDKgCBPBgAGpImAI+sDQA3QRIAolEfAL9+NgDkxT0AcLlKAJ/vUwCytU4AKyVYAFL6UwAR7DkA7E0oAOJ6HwCIhRIAq5IAADo+6/9j3+f/lMXV/00p4P+WFdn/kdPV/1yg2f+mAOr/XDXu/zyNAQD9XAgA/ZMPAGPYHAAtlSAAGHcfAMsgEwBcMRMA1RX//5kWBgAVgAgALpwGALAp/v9/cvr/9zv5/1bm8/8+fu7/EBwAAPPiAgDD9Q4ASuAQAIkADAAWxRwAqVX1/wq3/P/qGAcADnPw/zbNAACAN/L/MxLc/xAB4f9YJs7/OfDR/zxv3f+5xdz/innn/zf/6v+Rv+z/JNvq/8sL4f9fDNP/UeXX/9V/xP+eH87/fpnI/+O+pv86Urv/UBqz//gTsP9n4qX/4Fub/2d4o/8Y5I7/d1GL/7Qtl/+A5pT/3raO/2azn//saqX/Aomg/7s/ov+GA5X/XeKm/26+m/9udJf/35mT/9mviP+kGpT/Dv1x/7W8g//aCXX/7RJo/5mYY/8kM1j/CF9q/x7nPv8CHT3/SCQv/6AfL/9YjR3//KIH/zDqFv8tVwb/kuEM/7vfEP+sZiH/bk0q/zYyNP+LDi7/qi8+/2O1WP+ItlX/OPNh/1qhXP9AL2b/9SGE/6Y/fv+u4JP/Vh6j/7AKrP9pl8L/nlG8/1UD2P9z9dn/evT3/+758/8Wlff/X8EMAG1M+P8qy/3/emf6/7W+7v8rJc7/ubfI//Tfwf9I5rz/j0W7/1KFwP8vwcD/gtTL/1R21v8Uotb/18Ph/0Cu4v8iVvH/3uv2/1/s+v8qgv//H2kFALPjFACwFwsAqJECAMXb8f+wIvD/Q3Tw/5hX5/+eS+n/Mfva/wsy9P+a9/H/HKUCAMPPDwDjVRQAe5MoAAbXLgCGnTwAekZEAHnAZAALW2sA0Z97ACunjwCArIsAvJGUADRynwAWfqUAi8vHADKlsABKR8AA4a+xAICurgCD9skAbxKpAGsC5QDnyNsAUSbIALvvygCyucMAYK23ANN+qQCCH6sAJ4uuABBnoQBqJ6YAqjGiAA8OqQB4RbcAfLWoAEHlsACihqQAodSfAEBMmAC+BqIACxGfANfEmgCcOJ0AebaYAKnlkwDnW5QABZ2dACijrQAjmbMArWjCACCS1QDEaNQAeTDiAAbH3wD9sdEAdRK2AIJopAAkd5sAY72FAA48cAAsJlsAQHY1AKlcGgCORhkAEwn//5nV9/+hlvz/Nlr2/zqKAQCyZw4A9bsOAG0xDQCNqBUAmHsMAMo/FgCLxxoAt9ELANNPCACjDPf/NYz4/8g84P/oaM7/jwzD/zXDt/8e9bL/wty0/7HPvf+lPbX/iMTC/0QHzP/JUOD/sdbh/yDD9/8wSO7/8HPu/x/2AgBdn/v/vT4EAByR+v+LUQkAccABANoYCQBTogcAfL4CAFMmHAD2XxgA8qQgABvrLAAJMi4AMiYvAB5FJwAtqCEAxH8jAIHeDQBXlAEAn4oCAOh94P9AAd//fbDk/zSH0f9Eo9z/fdfL/znxzP8eic3/jgW8/6RXyf9dWbD/Ahqo/zCIrP/ClK//5Uqu/084r/8h/53/BFqY/xZao/8t8pP/Hg+j/2pdrv+SNLP/r/+o/4JRqP+SZ6P/IaOl/0VRnv+1Kov/2755/1wRZ//42Gf/tZ1j/5/QTv+Allv//JZT/x9xRv87vF7//nZj/1zWcf/afnj/FF6P/+AQpv80J7D/ZTu5/1Lw1f82jsv/M7rI/7nI0f8/p9D/Xn/T/0a3z/+Ry9L/FWXT//GT0v/gA+D/SrnU/2uY3f9p9tL/GeXQ/wH+5v+hi+D/RJz3//Hp7v//LwgAOBP7/3o3BgAE/Q8AR2X9/82r+P8IXAQApQL//+KUCwCOhhIAASkSAD/QHQDbqx0ApC0bAPlVJADo8i8AK90kADsCOgC5YiEAJ/45AMWWOQCc2jwAa6pHAHxRPQAGzzEAn6g8AFJYNgD/uigA2isjANVRJwDOnC8AwuMiAK6NFQBKwgkAh24PAPYK+v+BCA8AeW8BAAVXAwCFzPT/rs7p/30i7v97F+b/HEHi/2tF3//Xm97/kTfn/55j//+wBPj/3xD9/8ov9P+9ntr/pQje/yZE4P+IFtb/NmnV/wzcz/9GV9T/FqXY//7j5v+6Ne3/fmDz/6o19v8jqf7/ShMHADBaJACDSzgABeo/AItMQQCHU0kA1rY+ACe1RQDInE4A7ZBPAAH7YgCFw1QAisVkACS6WwBZVGAAnHlQANmmXwBpt10AeyNBAMfXQgAU2kUA4D48AANlRQDsGkgAk/8zAL1qMACwuRQAYQgVAK8tGQBpuBkA8mIiAGcZJwAWPRoApPAfAMHUIwAujCIAy4waAMABHwBFbiYABscZAFHHEQC0sRMAM4wNAJSuCwA5+ur/OBfj/4nC4f/DK77/xRXG/6iyuf+Ssq7/JeSr/ws6sP/Qz6r/7aug/z67l/8WpKf/Cs+l/00Hmf949qv/21Cu/zjexf9Sm8X/Hhqx/xV+xP/NTLr/4lKv//6Dtf+eIq3/j3ij/9Olmv96baP/FteY/27cnf9VvJ3/NRal/7dZq//AbqL/YKas/zMxrP8/wLv/VQfO/3Gk0v/W4+j/PqD2/wrdAQCDOhEA7gIXAM6WIQAvyBYAp+ICAKiM9v/vsvb/LvfS/7CR1v8G28b/IsKm/xy4ov+J5o7/YUKN/7fRb/8Tj33/GMN5/5tzeP8qzo7/cPOL/6Arm/8SiK//IDWl/8F7tP+5+7j/3JCu/6bLuv8HX7n/yZGr/6IKsv/ZO8H/ybmu/5Mwv/8ItcP/Kr+7/0iuzv+yf9T/5lTO/6MA6v9Xs/X/FmEAAPkKFABbOigAtAI+AJiiVQBHCFwACVlWAH1UYwBZYWQA6MZrAJRAbAB6T4MAZgCCAJs+hgCEZIkAcWyBAKYynQBrFHwAUOiAANbnhQD9j3gATzttABbjhwAmsLEAwKP5AGtPiQGLQ9cBhHkaAkIRDwJ1ENoBKq5SAUYO7ABMhZwAiCVsAAtaTABs2TYA8bQlAB3auP8RO3b/HvkK/1Uxyf69raD+lK6h/vVkzv7n8Qr/MQVb/32Do/96CdX/+xv4/zhFHABSOzoAgedCAK+NUgD4gE0AjGEzAAVOKgBhfxAACxcIABu3+f942eX/eAa///jLh/8jAX3/aqR5/0DvkP8nGcz/uZMGAM8pSwAUHIsAc73SAH8Q9wCKhiwBMsZGAZDjWQFShXsBtDGPAfYLmAEvVnsBzSlIAWIONgHlGC8B7T8WASXBHAFy7gUBKVzaAM18uwDU45IAbdRZAORiLACpFgoAwjkBANWlAwBAmgwAwIsVAOzsHgCcMwsAiuH5/6YM+v+YjfD/M23j/4JI8v9sBf7/mTz3/wU57P/UxdH/p36o/15LcP9yWzr/hPgW/zfE+v6tsPP+Z5AC/z6WD/+2T0b/CwRe/z3Tj/91H7z/HEHn/21sCABeciEAN5cSAIbNEQCnUPf/em7I/x9Tz/9xbJv/ZlOV/zPugv9UMWz/MuRY/zJ3VP+XCkL/C3dR/7TjYv/u+37/2N+o/+aJ1/+cjwUAkjQhAOqjQwCaME0AKhlDAJyKJwAthw0ApjXY/9B9xf+/wZr/SnF8/9mbYP8O8UD/j14r/0o0E/+paBv/BQEl/8xQNv++V1T/okZj/2Jskf/EQ5H/lnuT/8Pai/+s6HH/1tts/9QbY//lqHf/Pqp8/zULgv+6Vov/PiSi/xUmnf//dqj/Ismk/6bdtf8gW7P/eq62/7ucyP9x6sf/nX/F/+fFyf+Cfr7/5dOx/3Vtu/99arP/AlTI/5usv/9nas//upPd/xZK4P9lkfj/D9EGAPazFAB8xxgAqpQlAERxJQClYxcA2+wLAKbBEACyDgQAFvfj/y2m5v+2fdL/XCe4//2Prv+8Zaf/sC+V/4E9lf8h7oH/aF5//3oNev+KQnH/7N1r/yh+af9QDXn/CTFu/7Cygf98zoH/WXua/65Jo/+cD6b/HyXA/19Ix/+H49L/VYjn/8mY6f+EzPn/H6YGANEaCQDTgRMAwfQXAOzoHwA/oCQA2K88AHFHOgAhL0cABJ5OAC1mUgDUhVYAznJWACreUgBAUlQAtkxXANdfTwDqWWAA/chtAIIEYAC6WFQAYnpaALSGUwCOlWoA2JZxAHZEfgBw/JcAwmalAJlHuACt/80ABLDcANud5QBtwukAd2//ACpa6QD/bPEAKGTsAIy70gA/n9oAg9qvAK7zuwD1Aq0Af8aeAK1rqwDIpKEAeGmDAC44iwC0xIcACMmJAFUjjwDSf4wA7fSNAJI9mQBODpEABLh4AIjGjgAaDY8A93ycAIbpjQDOVnoAmQlrAJ+GZADon08A5GpGAE22KgCnzh0ACx4nAFkNIgCdpCcABX8RACC9IAASPBMALZcDAM7DFAADpxcAY68oAObHMQBjxikAH2g9AIVkPwD66jMAxDszAAQDKgAhMCIAF7AYAKJ5DADB+wkAXhkSAJqKFgBDKi8AWhwqAFsKJACVJBwAsSYNAOjKEQBmVg0A0Q4pAFkoNwAKBFgAqYaDAD2TjQAHkpMAaDt7APfiagA04WAA8/BSALUvQABAcDYAA6FJAFR3SAALEU0AFjpWAOw+QwCJJ0QAyL0zALYJHQBGqQ0A9NL0/3Oh6P+H4+//Nz34/zRaCABe9hMAvHURAHhzDgBUhf//wo70/8K14P9GQcj/wt60/1pusv8ykZf/gUiZ/wtek/+8wo//wvOQ/3ikhv+hcYb/QzV+//PIk/8dZZv/iDGh/65kpv+/ZZf/hzF7/4Wrev8iV2///TBj/9ppZf9MklX/RaBS/weyTf9Rfj//kztC/z4ZVv/oRFv//Dlr/6J8ff+mhn7/EEGE/8ene/+8lmf/qYtU/yphWv+Lrk3/xTVn/+X6f//WCYD/+hGz/4dkwP/AD7//Q9fQ/2kfvP8pZLP/In6Y/1Bhiv+uU5b/EvmT//KDpv91KqL/J2Os///frv/+LbX/9WGy/3vSuv9FvMn/TfDd/4lb3P9e9/b/tbELAEP2DQB0WgIAsNDy/4MO3v9qOrH/Pba//12Epf8GBq//JNam/2Osqf8v/Jv/TMec/zoMq/+xF67/jiay/yHBqP8TacT/GrfP/xGp2P+EquH/E3n6/6RX6//yJvv/FT4CALTi8P8Tgeb/Ceja/1k8xf/kXr7/eazC/z6Wzf9Z5er/Z/P+/4U7HABOAxkAmB8dAHZMDADfvPH/PT34/1qk+f/AJPX/Umv5/8T/7P8MVub/78jd/7j81f9kaef/qavZ/7Wk3v/kgNn/N3/Y/1Iryv+RJc3/eN/h/x6cz/+KGNT/nXTI/xfHyv8uBtP/pCTc//rB6/+1h/3/SAUFACPIBwDRmRcA9/AjAF/bKABRQCUA8H8oAHlELgC5gCoA/v1LAM9dXgAg61sAmt5pACalYQBXb08ACJpaAI5HVQAMME4AUDVYAP2RUwB2FGUAImdxADggdAB0wnEAjWVyAFtDdQCP+4AAEjWPACaAnQCk0pkAR9mcAOcZngD64ZEAtRSBAHm9ggDXh3oAWcFuAL4VZQD9WGIAQPlbAOz4VQBC1FYAulBVAMElYwCOGlcA0A9aAByqUwDl0lkA/L1aAKx6TQCMl1gAMsNcANLTVwD8y1oADspHAIYTRABesSYAc8AsAEL+MwBZhRYAHDsfAG3xFQDjRQ8AsCwLAK1TCwCf+gYAkjENAPtJFQAgjSMAIKUjANvcOgC27EIACkFMALM7TABGDkAAZlBAAK80KgDWCyIARAsMAAcTDwAncQwA0jgQAM6bFwBKvR0AeFc4AOUsNQDjtiwApdwyAIXTIwAUaisAR3YmACF3GwBTDS0ATJAeACaGEwA5NAoAqgUEACE4/v9uYPP/u+Dj/xsP3/+VP/L/8F74/5eyAAC38AgAXTQVADR2KABNtR0AAOoeAJUIJgBowiEAw20dAILDAAA3ifT/AuLn/8+h4//fsPT/7Cbr/1bZ7/+ZTvv/y4P6/xqz7f90EfP/A6nr/8n55v+3Fvb/0u71/09U+v+um/D/FPLz/yx84/+f+uH/1KHt/x6F4v9Z/NL/a9PA/xj4uP8WK6L/HGac/yz0pf9L3rP/ql3D/+mQzP88Ucv/vvrY/1T61v+umsH/kUbE/zpYq/9ZdLD/3Zq8/2iKuP9fbLb/ov+2/3nlqP96kKv/S1Gm/zaziv+1bqP/t3mZ/+rJmf9G3Zz/C3uY/9glkv+1e4b/JnWN/7Anm//mtp//G3qr/0Iysv+b+7D/TUO5/yjHuv9Xt7v/uHeu/62Ksf/Mran/dDa0/zBRw/+WnNX/ZT7d/8qy3v8BE+P/HCHw/0Wm7/9jj+f/S5La/yjvzf+D5dX/jRPf/2zD5f8W+tj/3B7w/xW08v9aXR0AB0Y1AEj4OwBq8kgAXo02AIAlRgBxejkAYBIyAIRZMQBaHRoABQYfAB15CgAMf/7/zsr+/0xrAQDBQAoAuUP4/5yrAQCBiPf/XCj6/xLYAADNTfr/UQ36/yTm+P8yPgYAgJgJADTfAACe1wcAdXwFAJlRHAC5Xg4Ao/AJAOFJHAB2sQoARMkAADiv+/+HIAEADEr1/2HeAQDUWgEA/43//75T/v9d/fX/r/Xm/wLU7/8fnuP/gD7W/zLY2/85LtP/STrc//Nj4v9HWO7/8NDr/6pc6/9gMfP/9ov3//zpBQCspyEAYY0kAMq6GwAIZi4AW3QyAOw8LABByiUADWIiAN0CIwAkMCUAlLcpAASyMgDs1zwAVA8wAC3INgAxFioA6+AqADC1JgDbghkA+kUnAAWuEABfEyUA8fwnAFJZFQAZWhkAziYEAH8aIQD/oBYAvb0YAGu7HQDb9RcAi+IwAArjJgBO1y0AcNYcAEKeEABIwQkAttn9/z9WAAAcwwAAQtoBAEPxAwBZsQQAsv8JAJOd/v/Dyur/twT1/wz57v8W7Nj/A3nh/1ui7P/YgfD/mIz4/zKN+/9e0gEAIewNABOT+v+m1f7/NQH1/zrZ6/+sWez/X0rh/6Dv9P/vGeL/+c7g/8zl4v9XpdX/aYLM/zE/y/+7psv/nB7Q/8+b5P8UIPP/nZv6/8vTDQCcMAUAVk8JAIFzHQA03REAk28bAFauHACIlSsAC0olAIU/IABweiAAghYbALQpHgDNpxgA36gtAJfOHAB41iMAHW4lAIHqGQAMticAARMhAK9fIwD6Qh4AJzQbAJxUFAA9mx8AyGYSAD+zDQCl7hYAfWoaAPqmHwBzeyIAJIk4AL4SKgCmzhgA+l0rAMuHKwDXzB8AowYYAG2+CwAsJBgAzPMEAFhmAABSdwYAAlkEAJmpCwALVgQAETkEAKzc/f95Z/H/pvn1/9bKAwDTZw4AsAESAOzoDgB1lxAArvwHAN4hCgDMYRMAqKgAALYuBgDPPgUApELw/+VR+//pPPf/ODT8/0ba+v+Nx/z//4QJAMPM/f/bZ+//kwnu/4H96P/92+3/EdHx/yvl/v8Qevb/G77q/3JP/f8L+Nf/C5PY/1081v9xoM//InLb/0HhzP8YgtH/pUfd/2bl1v+rtuT/k0jw/x6v3f+eMtD/aPjX/5v12v8zVuH/WxTw/8PH4/9E7PL/saHs//ag9P9CnvX/Jlvp/3Xu6P+HVeb/jPHz/7Br7f/e4vj/hIP4/38Y6/889PL/dcz//yKECAAFMgYASd0HAAR1CwCWK///TMsAAP0EBQBfOAkA3z4SAFuHGQDhIi4AO3AjAPK6FgCalxsAzk8RACrpBwCPJAsAAaELAPs2EAD+Mw0AUhgIAJ0SBgBG4/r/EKb+/3XI+/8D0/3/N64AAA/2CgD96/n/thz1/1UNAQAOtu7/WTz7/ya/BgBDKA0AgPIYALvQGQDUryEAs30hAN1vGAAmvSIAnJMdAC1mDgA9gA8AOFQLAEEhEADlVg0A3IgIADK2EwB4bREA/rEVAFJyFgD2XREAyLn6/zU87P9ImeT/YfTh/3Ar5P8gFt7/O8Pr/wwe8v+ODv3/x9T+/28iCgB2EhsAzgocAFEnHwAa/SAA0/8TAJnUDAAMngQA1uIGAAxxBgB84Pz/X6cKAKnfCABXxQgAXQvy/1Ux5f8fCOz/0hbp/5dH5/8kdNT/C/HP/3OGzv98H9D/ICPU/80Bzf90Mtb/81LU/9aH0f/Kl9X/SU7T/yIhy//uYcH/JpnG/zXl1/+NhdH/YTfL/1EF0v/nBNb/K1Dd/yfRz/+RnNv/ZtXV/31R2/+JDuj/5wPq/+KP9/++8ez/Qrf3/2h1+P/Cs+n/eBDy/6wH7v8enPD/SV/t/xJV5v9AS/r/+Rn5/9j3/v/DfBIAOT4NANiDAACWjgYA064MAGgzDQDIgfn/5M77/30TEwAOkgcAKEn8//zICAD8cPv/oh7y/14s+v/Ozvj/0df9///F+P/ucu//nkLk/8Zi6/8wfvT/jR7s/z5d8/9NdvX/mnz4/4ixBQAT+gUAlKwLAPAIAACOuAMAR0b+/yYt+/9aNfT/8X/s/8kT6/+fEuP/gTvr/9BU6P8tw/H/r7kAALKbAwBHzQoAntIfAMzkFgAPSRwAmXMjAIEAIAAnzxwAdnYdAGUgGQA94iIAxnYpAPAYJgAcuCcAn3crAGtsLgD2tyAAEgMaAASKFgBUzyQAFtocACavJQCNqCoAkYUxAKziOQCn/j8Ah/ZLANYMTwBFBlEAiGRIAPDTTwBGREYAVbJDAPbEVwAYOUUA6vRMAPuwWgCbPVsAghxmABQ7XABWOWUAk7JqAMmKcwCQbnUAR9Z2AKZ6cgDL2W4AZbBhAJ4nXACCIlcAC8ZHAK1YQgCoKD0A4+k7AHgwJwCspB0AujMRAGiOCQCvZhEAPdH5/9mA9f9pWvD/SpLi/yc33f/73Mf/1RXQ/9FSwP92Z7r/8ua7/6lNvP/pX8L/4cjD/45XvP+G9bT/qDa9/5bmuf+KDbL/mF2i/yCUof909qX/5PGr/4eZq/9ssL3/ohTJ/z6fyf9Gh9f/2ALh/4M05/8kt+L/dJrn/6r/4v9+UO3/KoL3/4+G+P8R2/j//tz1//y0DABASPz/q3z6/5U4AABLC/n/amL9/x0z9P8OGv7/E3UJAEFX////4AoAffgDAO3o9/8RMwAAM8Tv/+xd9v+vDfH/6iz1/+Qm9v8d8+P/rR7m/7f53f9Kitj/EUrh/1422P/qDtL/OhTX/2wyzf/ZSsn/6afJ/2CXzP+MvtL/z5Lb/8q93v8R1NP/sFfY/6wf1P+vGtf/WdnY/19M3P/M8Pb/38vr/8Bn6f9UgO3/4snj/0Id7f8RR+v/VGrr/+me5v9S597/QLHn/zuL3f9jtOT/JMPn/wPH7f9kGOf/OAvl/29h4v9mr9n/6Evk/xQ91P8hNtX/B3ne/zb23f9r59X/GvTY/2Pc2f8UTtX/IVbg/w0d3v++k9n/oArl/yjj8P9rOvH/P4bP/6tSz/+6387/NwvM/4iJ3f9chdP/K4DO/0nX0f8l6c3/c9/g/4j17f8Rsuf/7bfn/0eL3/+/K/D/cFrm/7XQ6P/nRvT/F0H7/+7KAADsTQgAH4MXAB5OIACpyiMAOI0lAIBNOwAKbDcArSAuAIQTOwBmNj4Afwc+AHgRUQBB108ALphVAIP2XADt91gAbgFfABBbUgAZxFcAjWlpAFiKWgBpG24ACNVvANovYwDxhW8AdENsANKQdQDrtmkAdBlxAHGwbQB4mWIAapRsAEUpXQAPl2EApuxRADBJTQCxVEsAOahGAI92OwD6XywAb5szAMfxKwBGVR4ADTELAF4yFQAMTA0A284BAI9v//8WHgAAi2vs//CT5v+uy9r/kVPd/7CW6/95pNr/M6Pf/y6d2/8sSuL/VPXl/7+Z3/+gPOL/2Pnl/1K1y/+MqNL/g2bK/xLTzf/1vdD/t22+//knz/9PpcH/HBDD/wD5xP8D8cL/g+7M/+sU1f+KddX/Aorg/7342v9z9Nb/iZPm/zDL2f/ykNb/KcXY/1u/4v8X5eP/VF7q/zpy9f9mgfv/hZkJAJmJFACOiQwALaH9/7hpDADYuw0AcVQRAASCFwALJigAm/sxANr4QAAgqVQAGDRmALIQdQA5F3YAKmGTAO0FmwB5qZkA7nG0ADOEqQCvHKIANyOzAN5zrgCUpLMAD8CtALhiuADcQMQAFnm3ACHDsQC1PrAAS4SvACy/tQBBoq0ATBOoAOihnQCF5I4AWm+EAKgZgAAv3XcAAWpbAMbfVAAdrUMAyrgwAJHnIwCpmR0AO2ESACgXCQCS7QUAGbD8/82b6//lY9z/6Kfd/4D+zP97Abb/wKml/7hFnv+obZj/ASeI//mNcv9KW2H/BedW/0tlS/9WETr/U34f/zcVEv8DsAH/sWfl/jQV1P7XvL/+zYux/o4ptP5x35v+6peC/hIDdv6fV2b+DdVg/jSqT/7k40j+okw7/gqnNf6KgDP+63ki/lN4J/42Qjz+8jRC/km0U/6j+GH+ib5z/uT0mf6KMrX+EXPe/nZKCP9wqyr/PlhY//9Dif91ua3/vYHr/9k/IADDxF0AueinALt86ABfvS8Bvh5vAddSsQHt9+4BkastAjZUbgKWUJ8Coc3PAj+Q+AJRMyoDk69LAwavXwO/nHsDACOLA/vvnQPRiZ0Dj32kA/majQNxumgDP7VDA9ZQEANIHNICHlqYAuDHTALp0fYBKbeqAWfXXAGPcAUBgiu3AFiRXQAyygYAQsG7/55KYP/VWC7/UUry/rLh1v6h3qb+rQl9/jrnYv4LcUP+9oE4/o8XHv5HWAv+JZTr/RQX2P0Bx8X9bI6t/Rlik/3v2Ib9fDVs/ROtUv0l3VL9Rr1B/UsGOf36QjH9h64m/annIf09vSD9Vpov/brQKf1xfiD9yEEm/bosMP1WTjT9+Rwx/avfMf1PJTH9DiU8/eGENf06BkD98HRM/QhAR/1x0Ur9KqlG/fsfWP36+G/9ns96/a52k/1bzt796FMU/v8CRP5gIZv+e2Lj/hJbVf/fN97/oZJWABKTyQBwIDsBZjfSAePZUgITXc8CXgE7A4+ulgMwMQQEc2hWBAf1qQTJ9uYEFDshBXxORgUj6loFxmaGBRz0pAUB9qoFc+ClBWHijgWIx3QFw4xlBcluTQXwXBUFGOrVBFBHkgQRkzUE66TWAzz5hgOWrzoDDTTHAsEOXgJAr+UBc+J7AQbMHQH4dqAArNMqAD16p/9Jm0L/00T1/mHOuP6SlYL+op06/tgZCP5FrfL9zRzv/TBt+v1fiP/9YmcP/paaBP6duwv+pcIw/q7jR/5mGmD+rvJf/rMPa/7pwWb+gk1t/jPIef57gH3+E8dx/qIRWP40Jk7+VpVP/hgfRv7Qf0D+GDc0/ieOGv4KRRP+3QAF/kERAf4lsOn9tLzV/VWLs/2bVZP9ddl4/U9nXP08XEP9lBwT/YrQ6/y+08L8knyk/KZzdvzhZEj8Q0kw/DpiDvyTw/L7fajk+yjPzfs55dD7ISbl+zPP7Puo3//7uBgb/ArjPvy3+oj8EG7q/JZ5Rv2HLKn9TX4h/km5nv6/Tz3/AcwGAI4iugAGQmoBEz8BAmeYnwIj30MDH7zRAxpkXAS34c0EXQ00Beh1kAVRGfAFtvdGBpBukQb7xsEGkeHBBrNH1wbFrwEHbS0RB+pkIAdEdgAHwJzbBnpduwa+F3sGUBJJBsbRDwaf3cMFjOhVBWXI3gTKnGkEH/bxAxszfQP6ReQC66UvAr5XhQFwrO0AO2FXAAO/zf8b5j//A/6r/tM6Mv4ouMn9jsSI/SaDQP0WiAL98iLa/FfguPzU+838V4vo/KskIf3YNUX9xw5T/SY8h/3uqr/9nbH//Q/eQf5su3P+0PWR/qLzsf7sxND+tLvy/ouwCv9SviX/u2wx/9YJGP9CZhb/a6ga//UYDv8gegj/GIbv/kWm3/5lI9P+VLTF/mCEvP53d7f+Trq3/lAwqf7IEqf+kU6k/j1Qlv4ntov+TDVy/tAWXf70sD3+iQoX/gvaBv4EXPD9aFPi/aB1x/24FbX9fLSf/aA4mP3GIYn9f9h2/apAZ/0HrFj9rzNZ/eHCVv0YkVn9RsBf/fabXv16PmH9uvFw/UPXc/3Bt3r9qtqB/bvSj/3AeoH9yuab/apY3/3O0Cv+Uhd3/rwEuv6Ofij/P1W+/+bNXgDkHg4BqfmXAbFuAgJn85ACBu4mA6vxvwNdBjQE9FWCBEhYuAQQ+fgEyU1UBTrQkgXu0MUF+X3PBbX2uwVzE7UF/F7DBQRr0gX4ocEFQ3CsBc8ZagXq/TcFp/kXBebj/AQ8PdEEq4WHBJo6GAT8UYUDaYYsAyvvzwL9NGACbMTQAR9FEAEEU2AA823N/3CXYP8j3Ov+wMxf/sM84f2V/V39HOAQ/e07+fw4DeL8bMy//JMDq/zBZKn88KrE/JoWBv22pUb9MTJ+/UwKvf0T/OT9gqIm/nXHfP7J5M7+INgS/6cpOv8MgGn/IGF//3turv/H6s7/R+be/9dN3P9CVsb/OYDK/0r51f+6vuD/ajjz/woH9P9qDOn/B9Lt/8wF8f+J9Pb/MTHx/yJd2v+odsf/KmKq//6agv+kj13/6bc4/5i9Ff8hl+j+A53V/o5Mp/6r0Hf+c1xb/pVaMP5kDPH9nQGx/VuIl/2H5Gr9rDEs/Tqb+fxRANj8kC7G/JeCpvxLZZH8qVqI/HSXePxWl338SARv/JRsavz5aGz8Nih+/LrwivxA75D8zFSh/Be8pfxSs7v8nXXj/E8rJf1hC2T9v+2r/YXu//2U0W/+egkU/3Vx2v8LG44AdyEiAVRGnwEUM0ICHD8HA1lOvwP3lkYEy7iRBIVj2wR2cC4FWl6kBUgx+gVMkh0GkrUrBgYuDQbF5gAGNTshBvKdOgaMrjAGk1rvBTSqhgWV2y0Flv8NBQ5t4ATQrJsERE4fBNVDiANY5vwCw4+HAoXuSgIR1tIBkSU3ATnmgACAvcv/qnxo//H5Jf+tZM3+ZM5l/ixF3/14eYD9YPNh/RTDdv1NwJ/9sJah/aSMm/2/wpn9LwnS/RYeO/7r1KL+WKXT/vOH2/4XYwT/Wjxd/9pmwf8YABkAY4I9ALiEOwB2ZlIArcV0ADfQnQBMFa8A49adAL6qeQBlqE4APvEzAK72JQBCgiAADTMHAFrox//Kx4n/GDN1/xlbZP/aglT/gIdD/wPj/P7+UsD+Bv6P/vcMa/4kSFP+XbYY/phG4v0pP639g2iE/c2ud/2Nx1L9aZsv/THp7vyC06T8CKV//Lx/Y/xCBFT8ooU1/PQ5I/ysmP776l34+7G+//tg6wn8Cq0V/AO6IfyJxSX8tIwf/IuxQPx/n2X8u5V6/LgmhvyC/5L8xjSb/NzXrvyk0tv8rQQe/ao/XP1rYZ/9cpzW/RDAXv7OWfT+5QTA/7E8fgCWGyYBHcrPAWVHVAKEnSgDf9fvA8QJowRIQhgFXRl3BW+zvQWDWSMG2/aiBkop6wY63wEHldvpBsRYtwa1OpQG0P2nBqrEoAYRwWIGP7kFBlsMhwVatiwFta8GBdSl2QSCjoUE2w0ZBBnqjAPsXfYC0RikAuPRYgKFD/4BsZxzAbxb3QCHcEAAIHzg/7CgrP+5dWL/UewI/wSOov7Y+U7+tgFM/iLfe/5qcpX+Qumd/s49lP6krLL+8vzu/jtwT/8lL7T/D73v/14ZDQD6c0MAGIKmAIabBAHcTFoBBnRsAW/+ZwFdLF0BtedUAYJVYAEbAU0BlJwcAebltgBgI0kAMLMDAH3+zf8i4JT/WdQ5/0DU4P5tMYP+MsZN/vvcPf6zsSj+yPAG/rapuP1+fZf9a0N+/dUhif1duJr9Yox+/S1Rcf3J00j9rhsl/RrPG/2PWRT9G/T5/IPhy/x4BZz8oC18/H7IcPyhUGr8mJJj/A2rTvwCMUv82E1P/IhaUfwG0m78eo1+/HX3bfxegGj8appy/PC/d/zlC3784Kp6/MBcc/zM2Wr8fAdx/GtHb/zF5XL8l9t1/MQsf/zlEpX829rF/E/WLv32MHP9hlDg/Y+NYP4GtAP/JpLb/2UauwDHCZsB5fBEAn62GQMwT+0DUnXVBPq7lgWn5QgGrwBcBmMjrwaKYBIHM+RGB0IGhAcBkHcHtXY8BzxX9AZFu7sGfL6gBnFIcgby0x0GWFWRBZelHwX/EbgECEBxBOXWNQTumdYDnXtXA/+kugKaG1IC+VMAAjSnqAF/m0IBvTKtAH0mEwDalpD/wplL/9FhGf99Stb+2Hd//vZ9IP6yC+v9P871/avhMv6KU1f+Jy56/mXktP7Z5/f+VS1g/zKX3/9ESkkAUBabAFTC6wByHSsBnUR4ATCl0AEt1P0BX4MSAjNg+wHyA+cB+7nTAWD8twFXdn4BsbgPAd0WlgBaPDUA4rnf//lOhf+XbSz/p0O7/v+kVP7/w//9lxzK/UkgkP2lVWT9Iagu/fdLAP0PbuH80L/Z/Mgk5PxpTe/8RDn8/G7w7vwWaQz9YjI+/S/dWP1J9GP9/pVi/UMhSf2jWTn9Q6ZF/fCmQf0eUBr9FGsF/RnI2PxRMMj8VuLD/OoQtvz+Ean88PV5/NaIcfyQKWD85PZd/M+ma/yVklT8xDUq/AzsG/zfzCv8zVYk/JfQMfz9sA/8hzDZ+yyb1vtyG9D72ZTb+09F+Pspki38nKpd/C5jwvwoi0T9vC/X/QeYvv6gA9n/zlz9AMyf8gFEC+sCpGPnAySL7wTiSh8Gu8kJB2JboAce/woIocBlCDdkrQjS/AwJg+M2CXl+7QiaUJwI2d8cCKiQowdQN1kHnZQHB3W5agZmRJ8FFE/dBF2tPwSiBt8DNb2AAzXzEgNYaX0CW+bhAUiNXwFxhO4AaZKoAFhdWQDszuj/Kahj/zkd4P6hnov+KBhs/kSuZP4i/DL+Yizn/VsTy/2Ontv9Zi0c/sped/5Wi6X+J2/J/s/x/P4dmGj/fUzS/8YzTwBosMEAD9IBAWC8OQFhvHYB6NDcAVpgDwLzLUcCjmsrApz53gHN6LEBNTCQAcmpZwH1ARYBHDeuAPUiEQDgF53//DFG/2Vp4v4ghnv+/hMR/khaqv1xLE792ncw/cujE/1ZO+/8BCrn/O780/wgVcf8FDrt/LVDJv3R4Un9w/iL/dLDp/060Mz9QbsG/nsJPP4R1FL+nzNQ/v13Xv7pkFv+mHtm/trRXv7xIC/+Fzrw/W5Pvf33vYv9KtNl/ZBJFv1Nctj8MoOu/Fi8cfy4kjf82hAO/CJ/7Ps/3bT7fduS+97LZvtubTL7DQQJ++vhA/uQ/tj6r22e+nPKi/rxdHb6tQeL+nAyw/qeeen698MS+89IkPvzZxz8KxrK/OvfqP14RpH+RiK+/w5vRgEH0csCpbIEBHftRQWgGGsGOzeGB7vn0wgPktAJ/c42CukxXAohvWQKRM1bClXVXgoecTwKEKqDCUjfjAiwPKoHKcXIBoArNQYRzKIFx7e0BCropANsB8gCgS4kAqHftAEn/4IBHAcnAaxGrgDaVFgAj00MAL/G9P9fMRIAd4cLAOF5zv/PF5X/+Gh0/7Xde/8qoKH/Igm2/5N8n/9KXIT/RyuH/3jBsv9CQvX/VRc2ANm+WQDx0XMApgKPABaf4ACF+UkBYS+DATM1pwFBsaQB4I64AQbYxwHgRdEBHMi2ASwLbgEAtC0BW8HUAGXQfwBsOTkAzJvZ//UjWv+ZjfL+/YWZ/jVRN/6z8vD9BM+9/fBPdP0TAEv9cEk7/cDnKf2NbED9jHpW/Znkcv1c0Jr9c2PS/UG7Bv6kQkH+Cu2C/kvYrf6ZWc3+d/ju/q+b//72XO3+UFHb/n2em/6bF0r+O6MF/iryv/0kmk/9lrfb/AFtgfx8ehP8Dl+l++ehXvtEPBf7HkO5+i2Vf/psszr6RqMr+nAlN/q8Ly/6f9kL+tXP/vlKxh36t2wV+sHzLfqI/zX6laIx+mUsOPoFs1P6ndWV+n89wPrKGgD7DhU9+6nUjPsK4g78u3vL/Drllf1aPVT+g76G/3sMwQAIVlMCASE+BKES5QVNUyIHRgFJCIMHkAn55qUKYsi4C1bOKQx1wfgL1tKDC4u3HQtlXaQKVnX6CSjpIglZFpoHV5cOBncZ5wSdvfEDWKInA940WwKsrlYB5HtmAHKVFADjgiAAgpBSAP73lQBjetIAA9AOAR66cwFrAvwB+Gt5AikJ+gLCJj8DgixGA3TLRgMteEcDJ/IwAytJCgMWcr4CGqY+ApNIvAHRx28Bj2oPATcZsABk/GEA+qwCAMyOrf9tFW3/3XhT/9ylLf/zHC//f4kl//cQHv8Y/jz/YB1A/2W/Qf8DCFj/mx6G/+QXmv868rX/sMy2/6kSrP/PgqX/Q+SQ/8sInP/YeH7/UxBr/2PNP/+YWSX/8AMB/5rQ6P6rnff+cZrX/vGlvf5BQJv+IHWR/gl8gP7JeoP+5Gd0/uf5Nf5URxj+zOD8/S1A0P3f+qj9cJ5X/fqf6/ymUaP8NQ1x/Ld5Ifx+9r/731uZ+2OuYfuajDX7CfE++/bWNvthP037y2Zt+4fHi/so4pb728C/++uq/Pvp9Rr848xH/AEOVPxBB1H80yhB/Kx7RvwPqT/8gvQY/FNdAvyc+rf70Rxs+3BjQfvLwS/7VDgi+9POL/sHLCD7msEF+2P3Xvvhlrn7Aw87/HZt0vzlMZL9rDNp/qSQcP8sUdUAWXRaApvETATeujIGrwu8B/bL8Ah+dh4KOgEuCwn0HAxOm8QMTDqiDIVkBgzJOTMLE8FWCjvGawnruV8IcpriBgC4HQW6+34D0CEuAmw3WAHqM7wABmsiAE1zif9yH0v/sNZo/yU7/P8FOtIAaLKaAeU1UALxAPICz+GtA0bUTQQaPdoEv2I9BbBmSwU+oA8FFuyhBIpSIwSzVJwDAjHYAgTi7wGS/QMBPF8MAHh0IP8w4Gj+hTLb/dEhSP3bL+X8j1uK/ChTZfxQhXb8DDaq/CxtJP0B34L90bgJ/ghSjv5FRCf/X8/S/+q6aACzUv8Aq+9xAVPnwwG0+8oBp9LqAVrE3gG2ZLIByAZ8AUoPCQGShXUAWZTa/1+0V/8y7cv+VotO/n/ozf0zdFz9ssgH/X/6xfy4JaH88HGO/DAVj/wVcoD8ZDBt/EpFd/x4ZHr8GSaA/LzfefwDyF/8/nte/IcKZ/xwJnb8pb2M/Gwpmfytz538chGl/L9dxPwKJ9b8wLLw/PiFCP0EYOf8VKLO/NFo0/x5sLL8FHhe/FleKfzkEMP71bFW+3kQLPuensf6uFdt+vk3SfrPXTT6HM4S+q30LfrTaWL6Vi6B+kti6fq3U2X779jd+0UOW/zQtO/8G8Je/ZYEyf1SvTT+udOG/rThA/9q2pv/FSIpAPmCwQDYO4QBO09fAnfDowPWt0YFN+cDB59+kQhabbwJK7iZCpxzWAvafCUMoNupDHy4cwwsD5ELpbIpCv3brwhRXmQHMpIfBmYzowRaR9EC0lwZAQLns/+IMBn/A6AT/+hqOv9cCIL//ELv/0/N3gCu70gCnUPsA5KYXAWM4JoGY9+RByCuUwigydYI6JTeCFnJeQjej6IHUmKBBl/gBAWcDD8DV4BxAb8ZkP9t99z9Y+Fv/B2WIPtYJhH6ZJtk+Zu8KPmAzWr5Qj4T+snq0PqOVMr7B5Qb/Z6hg/6jnOr/FNJoAVnqwQIq7+MD2N67BAOLKAVpU1sFGT9zBRVTTAUeuIoE8OhNA3X67gG373EAx+cL/wYdqf14nPv7BPKE+shnUPkg00j4xV7C9564rPesjdv3MjJH+DlWCflHjRH6z4No+/3VB/1gK2f+69Vy/7G2fwBKkGYBu3MKAh0RdAIiW0oC7BmeAY0KwwAbQcf/Npyc/uXdSf2ub/j71eai+rKPafk55oP4tDX291MJoveKn4T3JRam9/DV/Pd0+7H4tIyN+VrTY/qdnzL7S7AP/J+dAP2w97D98ihD/ng0t/7ovfr+HNEy/wOfT//Z+jD/Fffu/qIiiP449/792taM/erdSP261dv8vwlN/FfsDvxF1g384u05/A4OqvyINSX9BAyj/Sbhjf5NytD/zg8FAVCIWAIE2tQDkMdtBT1OYgfbw3YJlTIdC7YxIQyleLsM3LX1DODkAQ3+1t4Mf3YBDC0MUgp16DoIRTQ7BiJDfQRPFRoDJFWrAVXjFwBYvun+u1pn/gQns/7XDXz/9HiVAJaqqAGUWvQCN/WOBL/bMwZyFckHl2z0CL9ZtQnE6QQKUnnuCXmgVgmQij8IqJXBBo59/wThDBMDXs0NAb4uEv/cZS/973B7+wyCUPoOKJz5tMRA+RFaO/m8zJf5IXdl+hgwh/uCjfH815dt/uLE5v95rkMBoVFnAgsBcwN/K2AEEaf3BF1YKAXtoMYE9EIRBK0QLQNa8ScC5Z7hAJkvQP9Uh5f9FpT6+z6Mk/q7l335g6+s+MRPQPjCli74iplh+CYG7vgM1Qn6anlx+1N+4vzYlkn+Pgek/+nN7ABiERwCC/3/AlrOMQPP9g4DhOuGAuqNmwEpomoAP2Xa/kT49PwMrRr7rZ1x+dxw1ffxPaH2bvkJ9pAsvfVYv7n1JwBa9l21Y/dbHqT4W+wo+hpFsPuwzA/9Rct2/r2u2v+JY+cADbCKAZ/l0wGIvKEBaBRBASWnwQDWyfn/K//u/hfk7/2kmfb8qFMA/C+HO/vXJcn6LoiP+tmIg/qFMtP6DTI1++o+xvuwaIj8qWpw/cFEM/7zSvD+mlXA/5mFTgB1G80AITktAcIwPAEx3FIBcXPIATPgVwKYl8UCA204A/d6zAOfSqwElnVdBkWTKAgNJFEJ3fQTCpzbggo5WaYKtivMCldYtwq1pLwJEn4tCOpgYwZ1a5UEAkIfA4+RQwIV2GMBn0l/AJmQIAC7/SoAhJbjAGS8QgKPFa4D1knEBIXf8AXI/CYHn3wICO55pAiHIckIy0U0COYtTgf5TioGjOFuBHhcjQLm9JoAaTCx/vhuF/0Ahd/7KJfO+pWe/vkFqNb52yET+sfZufoLSMf760Tb/HxjyP2Z/eH+FNoYAEi7LAE43B0Cwoi/Auyp2QLX2+ICKKrpAnZErgK4Ez0C74eZAbUyswBnq5n/c/2r/nq5tf274tj8huoK/FUJHPtfh0P6FFvb+T3Ey/kaT+r5bj5J+hpfzvppKnn7HTN9/CUYwv2q2ef+zXLZ/zEooAA5iCsBQaBlAckcZQF4H/wAKHkYAFIE8/7c7Kj959NK/JuA//oYRcr5JKrF+HE5Jvgcl//3/aw6+EqEyPi8sY/5pcah+qof3Pu/bS79EheH/oD5gv8ffToApOG/AMZh8wBnCcIAgbhnAABdqf+p2rj+jBXY/R6OBv3krmH8Bqr3+34Cs/ujOmr7psp0+8mZ+/uxK9D8+auM/SVXO/4P5c7++3Fr/0J0KADfQsQAYgEnAUBhVwFErT4BQSe/AAPZZgCShkwAcw33/5tIhf+8fRH/Az5//iZRTv6u49D+0rOb/xJgdQBIu5cBnvvSAve4PQSzJ1YGMfDWCJRxCAsCN4UMZSlRDe71Zw0OnzgNue3qDB6y7QvOFRcKn4e5BzO1GAUm/LMCeBL7AOaSpf90u3z+hg7E/Tr7m/2BCtn9Kjbw/hBdkwA6yhoCbiylAxTKHgWvzzsGkDAeB+tP0AfKaeIHogKFB0YD0Aa6658FdrgLBLfwZwLGLc8A/b9T/22rD/6/I9/8FYrI+68eBvsA5cb6cxfW+gbfK/vaWJ77xn8D/HqSv/zMzLT97Omi/kNwjv9R9U0AUIzhAG44YwGQ6sABTtLmAdFI9wHsKMABsFVCAb7baQDBmVj/O6R7/v5Vo/32Z7b8ZOOq+6iouPoN5A76C7/w+fcCMPpolqv6oCBr+1b2PfyL2z/9PyRN/qhjYv9LTz8AdULIAGDs7QBJtKkAfAQnAGulbf9vaZb+P4N9/VSBWPyxx1T7tbiG+rZIHfqDU+v5vZLc+YarB/qGVHv68gEi+/i70/seoJb8MZpK/UOE9f23+6D+Tp4C/xRQLf+cGDH/ct0p/38WEf/C8e7+/8K2/oEUdv5vulP+4LxU/sZ3Yf7yiFX+8k+H/po1of7UIqP+nyK+/qkO5P5VgAj/DlYp/3L7Rv860CD/nb02/1Ygfv/9kqf/Ip+a//ATX/+wT0D/iTdD/9gTj//Z0rL/l0Ka/14Gr/9Xm+7/3gswAGPJeQCm3bYAZY8PAbup/QGDZUQDHGZwBEhVnAUMQQYHKxe/CAGixAqQAjwMfX2nDKBaMwxn6kILcp7rCdKJRwjciTQGwWNqA1sqcwDBHM39VHkD/BODKftlRSL7dnib+1nhd/z8sQP+M6FHAAuABwPllsQF6k7kB1dPTwnJuDIKfC18CnllJwqB8x0JCOF+B/wHWgX5FwEDoVKnAO5rV/6fBWP81e7t+r7NEPrpoI75ZXNq+bHkovnDtDH6ts8k++o5R/zRo2T9XD9W/lEmL/8dgdX/wY54AGsmFAEv9X0B2nzQAVFaAQI+lgECo1LVAVuzvgGzkJMBDMABAUKIKAC9+AL/ZtvW/Z8F1vwjDuT75ngb+48xc/oi1yn6Ly1P+oX5zPpB7p37XUGg/Ggis/03PtH+LHPD/8UDdgDFGvkA7K8pAXRM0QD1vxsAGntc/1H+Xv6Sj1b9OeRO/PfTZ/suUcD6+3dk+kOrWPoAkov6HpH/+tTBiPutWzX8nGTn/NAtmf1/vzj+ZE2w/v2QIf9MCmj/4BqZ/2nluv9a3bH/+Rqf/5/WiP/CWnL/l/tY/4WPKv92Zf/+kcbr/nYz5P7x+t7+R1LL/ojazP5GX8L+L1Cm/rGHuf5yfvb+liIX/y/eA//khPL+xTk3/8j7sf/TwhwAmxJUABoyXQDwIJIA4uHRALzEAgGMVgABjknCAPQFbwCVxfL/I0xl/ySVF/9DDXP/DCFuALZ/qgFwFO0CzyprBKm4sgZ1KGkJWVCtCyzpwQy933QMZ/tOCwgEvwloFMQHldo/BRaXSwKA4jT/Bwp+/PxWwvpj3UL6j2fs+vLycvzE7m3+NmudAKN5PwN7cUwGgq/PCNgNcQpJTQ0Llk+pCuXRmAkbiQcImTjNBcaXDwMsMHMAQd0j/oGMUvxXJg77Q/8q+gwz7PmzMWT6eK1I+1DjUPxk3lX9XYs6/glK9f6LgcP/pvZXALL9jwCJxJAAW710ADxSUAAeEWIACBusAHBt0ABQLcsAE+uiAA4DqADB298Av5/JAG5mJQBtDzD/72c6/sOoUv2LWYb8BwTN+03iKPvAVMP6hpDI+vHOZ/tvWmP80tli/WkpQP7GhgT/94bO/6U8WwAlvGcA1LIAAOM9Yf88dpn+TIyz/dRT5PyC50b83p/U+/0fpvttR937L+FJ/F8Yw/xGwD/9yGbI/ejASf6E9qr+x8rI/gfcxf5SztX+4jfE/oukuf7Lctn+h+fx/t8C8v77uAr/jPU+/0Vxef9BN67/sOqw/zije//IbE3/+yoc/3tFx/6Mpmf++yfu/Vcxe/2AMDz9RH4//WM8lf38lSz+Uvvd/rb5kf9WZIIAtyaYAUH0fgKN1hoDyilDAz5k6QK4WkACCjWUAdlUvwCXvKP/9Mxk/rOLeP24/OH8okKQ/BNXi/zkb6T8HdYR/UP2N/5E3vP/50LNASS58APNJGIGh/TRCK/JyQopS8QLt/Z6C602Pgpu+48IoRt3BvyP2gOARQ4BnPhr/koffvxklab7z6TO+xJIx/wlNEX+3ksBAG7B5gGiSwgEmKbsBVhqKAepEYcHFob6BoqKAgY1bdUEYdyCAzxcDgLP/ZoArE+I/1Ua6/4csMb+GTfw/iEMUv9G3db/rvtvACJS2ADJTQQBkcfmAMO3jQCjbRkA72KI//kt+P7pNXD+vvII/k6T1v23APj9Zzlg/lT16/5upjn/vGJg/2SjhP96ZL7/Eere/xbTqP/Zniz/hd2q/nYpRf4D7+79IM6//S4upP29QcH9+/kE/olLdf5etg//B+bA/1YAPABQPVkANyQ0AFCM0//Hc0X/q+J2/km3i/3nrZ78m7j0+0EwifvgaFb7JCSA+7Oi2fvkO1X8u4v7/FXLq/3jgDH+1xWH/qD9x/6MDe7+ZXPs/hG54P577cn+9obB/tO62v59oB//TmBx/18hq/+irPD/8LQeAEGAMQABAhoAGvus/62NJf//zZf+6knw/Xa5Sv2kQ/j8ogfZ/FUoAP1LcWn9Y47w/Yyo0v6rGO7/yBwZAagACgIR9bQCjlEbA516LwPXTvICwPCFArqHvgEMQJ0AhHRQ/z1DLf4Fpk/9Qc+i/OUHQ/zxFvz7DNHa+0xV/vt96Wb8gr3V/I3ltP3eSSD/t3XTAJjcvQKc9tAEwkr1Bvck3wj7xDoKsu6hCsh3DQoaO+YIj6AnB5vS1QTI5GoCGokdAFSPUP7aZVb9ynoZ/VwqZ/1t3IP+JP4FALD4mgE+FlADVCa3BGkzkgUL5/MFgIHcBXM3HQUtGkEEOwxfAyt3SwI/4GEBPnvLALLaigCSu5wAx1HWAHjR9wBQnCMBJploAbIKdAGkExIBpe97ACSqy/9mZwr/xBZ//uWuE/4cAsf9K6a5/VfA+v1kCWL+xccT/6ej4P+/EUkAohhhAKElTwCEWiUAFk7c/xNbkP8CAjv/Hivd/rvVtf6S8tr+9Gop/zt7mP8yrcj/dx7G/2TCu/8QNZ7/Nl5Q/8uPrf6yfOv9q4gp/TZUnPwwGUT8tssX/AqCBvwH/yL8jgqJ/IcNL/2729P90uI5/iuHcv7OxHr+KD5o/gYCMP43nuL9NtSf/S2HaP1+JVH9Pqpr/cpF7P0uN4/+ViFD/30V7//W5WUAnkTUADraFgFrYgIBiluJAAcr3v+d/yT/kLpq/ijIuf1PQUD93ZIL/e5ZI/0Ev3j9wy7z/WF8lv6OUWD/lIkfAMxNlAAE7t4Azwv7AJOWBgHqDiYBCyIDAWPZpQB38XIAbSYgAOzJtP8KDGL/+zfo/ou2Z/4U1SD+PiO//RxJJP12Fs38+rOz/AA9gfySvWb8/AZr/F9a2vxx1T3+H3ItADcbTAJeAOYEkRzQBxVjMwrfMssLB/D8C+Li2wp23hwJX62/BsgWuQMFiJ0ARWHR/XLtufvd6Bj7BqOb+0K/0fyuH7b+i38PATtUKAObHQoFVCN6Blsx9gZWGJ0GwVqIBeMR/ANL/24Cmio9Aa5ZQQAv2Kr/pZK3/4rjRQD6Lj8BS+VRAiTNNQNIkckDc6PUAyKlVQMa+l8CyoocATpPnv/m4jH+8Asq/ZMTlPyuOob8NQn+/MYn1v2DXOj+3M8HADX8DQHWg9oB1IVNAsgzMQJ4/VoBPk8rAEFQKP/3zo3+H9Aj/k51u/3N5F/9ZiiQ/VHXT/6Pqxn/ZduC/+zMi/93Wmj/DOM0/zOA+f5KBnP+Oa+6/RcODP1EcZn8mkFj/BA/hfzgo938w5A0/fVElP0KwhP+J7aZ/rkA/v7UI/j+2vF9/uRU8P2Ic5b9MFKE/S3sgP08l179SYRV/VRn4f1YC9H+7iGt/wCsRwCst5cAD8PDAPF51ADmMJIAwuAfAFyMiP8aetT+HHw9/ih17/2Vhu/9pgUe/hkEcf4eYKz+/zQT/x3zlf8X5/T/iwoSAObi1v/mTXH//jwN/8JW5f5neN/+Ov/R/heB4P6WMED/wcum/4Tv9/+8kCIAPOkKAHZttP9QBjv/AsOQ/olC0/0eLkT9r7nn/GLov/wrpMH8h6Tx/MOLg/236Of+23i/AFMzygLlP0EFJ5jjB5y9FgohR04LEM30Cv1eHwk7PaAGxr3XAyoM5QBxBTb+k9sN/LSoxfqxb+v6DTOF/JEiBf/9LdIBRQdPBADTDgbTsToHgWTJB/vgfAf9OCwGA18ZBOd50QH6XiAAartI/73AGf/pWG//SU9SANmCrQFHkzcDgneeBKL4cAXiWoQFgSjUBAljdwPgMK8B/VbO/8icCP4wVrr8O2P/+x288vuTIaH8ccvh/Yx/bv8WMekAK3nkATa3OwKDlyICuCKnAW0svACWJ5L/bQhD/k5xPP0D6gH9Emxw/bXWJP4oCNH+zNF1/02WDACrk5IA4fOrAFDyGQCBchr/mbwg/udUQP1F/638HJB9/Ocrg/zGJ9f82WV//YBmWP7V2zD/TCPf/9eZDgCL2b7/ldMl/415Y/7ncX39S5GB/F9hlPv0Khv7db9c+3hIJ/wEylr9wOu7/ieO+P9l0AUBvefHAcq66AFwREYBrQAvAPes+f7Krd395Fb+/L2ql/xyNZ38r/Im/Sv9CP7imwj/IjQBAL5CoQDMidAABKl3AIri1v8vbP7+t5MM/p+LLv2fL9T8ra7G/EXbBv1kjtD9daTQ/rwV8f87EBoBHAXFAY66wQGhsYIBgo61AMBjcv+9uRf+coGi/KLLRPvyAaT6DzyP+teT8vrXte77smZG/RywKP9zypcBtFsaBGMpiwYSVusIhktbCpPtkwqFj4YJ8msmBwTaGAS63xwBdClL/i1VR/xV9W/75kSQ+3SI3PwhcEX/8pY3As/RFAVj5mYHVBJkCCPUUAgZu4YHCWsCBvp6BwSKS8oBxMCt/9ZkY/7M4Vb+opo0/6f1vQDPsn0CmMcWBG7WZwW27hsGkcgIBihEKgXLC4YDWDpaAf1mKv8eJlT9V0Uq/LaBy/v3Ii78M/UK/almVv6zScv/hVgIAXf6CAKsHI8C1Bd8AlkN0QHGiq4AxSBO/zU8Mv55aKH9jdWf/cIv/f2XWof+6s03/+jlOQCK4yEBRTZdAcqD5QBDieP/o0jK/tsx5f3QaUH9c1u//N7ZoPy3B+z8L/SO/c1dff5n0mj/hov+/6EYDQCY0ZX/cz3O/rp9Af5//0D98Y+E/E9VzftEFIb7SQXc+2qUsPzAitH9Bc7Z/qfhif+nEQYAr2FXAENHTwAZkOf/3JAN/1j0Jv76MW/9LbAP/Wm8CP24tVb9J53i/QL5af47yyX/kvbk/xudaQC8t40AxulPAGLpmv8Pu9L+1s8p/n3Ldf20te/8DBap/Blw2fwhV6/9kg4D/6R+DADbz6IABN8PAUI5KAH7GvoAv+VoAGF0V/95xfr9GfPJ/E522/tNMmX7YsyK+6dK2vsxGSL8xwmt/IVWCf6JlCMAVTnDApmTsQW3/aMIhlv/Cj2VQgzGsZkLA4vdCCbZGQUn7TYBZtHq/TFSl/segEH697j4+S2Xd/vcpLn+XyDmAnRL9Qb7YMUJ4I+ZCpxeBwoZd6YIAcyTBtGSAgRncfMAFer+/a2HSfwuD5j80upi/rXOAgHtj5gDVBiUBRbdFAffDeUHRmXAB27FcQYG6R8E7JIUAZOqLv7s4Rb8uUXv+nfr8/oHMdb7Jg1C/agP/P57bNwA7AicAof8xAOqFvgDFufiAlqh9wCXcRX/jfjx/dt1iv3IwFb9hcZb/fo7/f0kC1v/lLQfARxdawL53IcCxMeLASOQHQCjjdT+CefZ/XLOIP2y6Yf8z8FN/Hbwpvz+DJD98CjY/gOKCgC6P4MA9pkYADYTXv/ziMH+IqRK/mWznP1V26H81tLT+9q4sfstpk/8tYpA/c8oNv7plvL+pkxh/8zxuP8QdOb/5hbB/z1gMv8VS1L+PbNc/YRz0vz4V/X8lax6/ZeXAv7PB5f+i3wY/1FnkP/MVSsAORZaAJef9P+LSQ3/NX/4/dlWLf07o9r8GBHy/AxwIP2/u3/93qdG/pEOTP87Tz8A6v3WABSRyADy9CgAo2t2/4hFrf58wfX9FtBW/drB9PxqD/b82oQ9/TU+uf16oT/+tGmH/oQZTP7ky8z9uRlN/VDsXP1B2HH+PJ2OAOOyoAMAtpsH0zIuC45k7AzXpFcM+jl/CcfKjQUjIscBxnRV/ha7avt5RX/5HnIX+Tuf/Pqpj/r+u//aAxdoHghAb60KLpAxCy9zUAqTK7EIautEBjH5BAOqbWr/04FD/OwE5vrKfvH7L1q9/kFPHAJdJxsFGWk6B0enZQgcXssIvGIKCP9B+wXT0PICo1GK/wYpafwLG1j6t53M+cGhm/pik1H8Rw5G/qqiKQATmgQC0gSZA1qkKAS/JTYDkjb9AMg6c/4iyZT8piC2+5X4ofuuh1X8OYDp/TYdVQCe7yIDB95ABWVo7gWmzucEZveAAlCwZP9QWmL80RQe+p0E1vhNPpf4nc9/+Qiyl/svW4j+7hlDAfaw5AKioB8DNaUPAiLrUACl/D/+BZkT/BYVR/p/QlX5dWx8+SV4vfqKo7L8z8Hs/iZPugBJ8NwBSnYfAhuYdQEEZTAAvXCl/qwPNv11MAT8Aapo+4ehjft8ulP8g8R//dfIyP5bbaH/ONYCAMyY4P9m0Br/7xIY/vrzCv1fp+v7evky+zrgT/udofn767Um/aLzof7rh9H/wdqqAMJrSgHWCDEB6KuxAJ24/P8AvPD+KBLl/S7fQ/1dnPD8H53T/JImG/1F+Hz9X1MD/vTuh/6A09z+xO7P/uBzXP7AiXf9oEa9/JBELP2+Kiz/HD+QAn1K1gbXyNgK7DM1DZA2Og1WydMKyOvHBg2HVwJiA1v+9MM8+xEIgvlWj4L5Xw2k+xqix//iduwERq6ACZByIgy4RFkM5UyNCgqqhQeaRBUEEySfAKIYhf3pmTn7rQyj+o/pUPzBXt7/NqtKBFba/gfxEuwJsr8OCiOP4QhmmcEGYTYSBNKT5QBGA9f9ddbE+zqhE/vjjbP7yFJ//QKTu/+hUqkBONvcAjKK/wLhWkoCs5ExAayvw/+Hew7+iCix/E3YIfxAtaf8pcAS/pQQ3v+OM5gB18QVA0bQ4wPwpZkDGB97Am+H3QAxJOP+mbD0/FYomfvBET37Q4b8+/sDbv00w/r+FUE+AEozEAFrr0IBBO6oACJmXf+R86n9qab++wGr9vqHHNP6sXiB+xXAz/z7Q2n+Q4Le/4ck8QA2tIMBqZJaAUQBVgA8S9z+A1lJ/fwlG/w6Dqn7bAyt+4KARvw9JmX979qU/iLLqf/xnXYA6P96AA0O3f+8Quz+TNCQ/ShnSfxbpWT7GWPN+ueJ2foPTI/7eIFs/L8bi/2zRsf+NDqj/88kMgCHwG8Ac/sRAPJjlf/rHhT/rlRs/mJb/P1SpQb+BFg8/nSimv7mLTP/AThz//NuZv9PLhv/Mzht/pbFWP0z9HL8S46P+zc/FvuUVt37PIU9/oAqXAJk7wQI57hgDU1DERC6iy0Plj7OCvjZwgR6NEv/eq9Y++qQ1/jU1w74n3sA+XqKKvwHF8wBAW5RCNsqUA2ozisPZS1PDVxl6Qh0QScEg2gwAFk4PP0cqzz7DaBI+v4WI/t3jWX+nv1SA9W0MggyEEQL2qGMCzyxaQmFshkG39+vAjyb1P/H/7b9bE56/OWpVfw1AlP9X+FG/4M1sAEFLY0DS4ImBDYXJgNDK+AAOgdV/jKwffz3cav7ea/W+zZIlfzYG7n9rpxX/xjvNQGEFLACaU44AxX9yALwx6oBhxU5AE18vf6mb7/9nFBu/QsQrP2EHg/+ZRt6/jN8NP/Vdx4AWaqlAOzLdABddJv/lu58/lmkbf1I7aH8FVUx/BrxRPw46sb8hMuI/XRDfv5pqmb/oxkRAF2rVACfluX/bR/7/pE2J/6doY/95FVa/eMXb/0JrIn9t+G4/SIcPP7HAbn+NQAB/0Bu4P6H61D+17iA/Z3X+fyfjND8Rx/u/LqbN/0LwE39ixdF/TxiTf1y3Xn9MHhs/axTJ/2aPX/8C3Ht+9EF0vvAJFn8yzkj/UGdP/5xaWX/ZPbu/4tbAwBiGuj/S4po/5ris/5D/hj+PkM5/UOt5Pw4vVj9O+UI/vmtjv7plO/+DP15/qPHEP69u57+wy4EADn0ZALM8JsFoqegCJwPlQpbAwULGJYNCbgEcgUNiUsBuEec/ZvkcPtJlkb7cwR//Azj3f5FLzoCTcjcBd/FHglhXgkLr2aiCutGGwjHfbsEI7uiAfvPvv9wgEf/6tiV/7uQeACG4iwCb5dcBK0Lfgb5290HTt7FB4+ZDAYIQHQDg74GAbCRcv8LKvn+gaFj/1WaUAB/omEB4ChtArFVMgMSkVoDDfK3AqYaSAHTdVD/cGNM/VMe7/tMyZz7tf5m/MY96v3SVZv/gpQkAejqdgInwX4DMwO9A3NVHQNxTgACRgqhAGX9F/9PP8n9kaoP/fU8Cf2AEmT9Soav/RkeB/5uPXz+Pt7//tJyZ//JgYj/kcdn/7T+Hv8eUcn+zkpQ/teizP1QE0T9lebl/JYx5/yS4BX9miNp/cYz2/2h+UH+JF6a/qCr+f7DbkX/Uqt4//LSVP8PZcj+TTwz/srdvf0WenD9KTM0/R1X8Pwp89r8ER4w/TvGy/0hc3/+agYE/+AO6/4SmDX+lcgf/bdyBfwg2Ev7UC48+1DCevu+ztr7HCet/J1vpP2Gr7P+Q/aT/+zX3v/d7Xn/bJLN/lPBG/7zNK/9LMqg/b2gtv0FOOv9teoc/mRwQ/51h1f+4n2W/l1luv6HCMX+6zKL/kUwOP7Nws/9Xic3/UTGUf2/zAb/uDWxAlUQUghTE+8NBKktEDx/tQ3mMGMHVfAzAH3YZvt8ZQn6Ai+x+ks9BvzGNLn9/mivAOm3jgVhIg0LnDtgDtUIVQ0VBiwILqe5AXvCev1Ro8r8QsSw/kHP/QCf65UCxiLbA4IfyAXzyisImOawCaSI8wiw0KQFzBIzAdWHnf3b93P80TrV/XREkAAes0MDcm/UBIxgCAXjZ38EMb59A8V3xwGK+iz/3Xkc/EeE1/nOJ6b5FdC/+93sVP/WuLcCXwqQBO+PYQQzSuUC7DVEASVY6P+nCaj+4XJU/c42Y/zzJZP8WZQ0/gNXdgDbzP4BOB/dARtfYADJe2/+9KgA/Y/laPx0Tk38u3OB/Fdp5PyKV479TLO7/nEtCADXh5IAPO4AALQsov4QKSr9ra1M/K0YWPyARfr8uwrc/TbSrv49XiD/GuqH/2Do4P/gFq//AFzE/oa0X/1VpgX8GYR2+/w2zfuEPI78o/Wt/WljvP4juWz/gwLt/wpXBQAXjzv/s2nu/R+xNPxvalX6q5d3+dQlvPmc0cj6c5ZZ/OTYv/3hfX3+QPXY/td61P5M56b+jElq/h6KyP2pmvL8nCyD/LAJvPz6Y1z9nhHs/Z1V4P087Lb9kiKr/dblt/2U0u39Xevc/UMQQv16mef8ivHb/abZigDGiA0Fx/wiCht3zg1VB3AOSyd3C5h8rQWy4nj/3PIb+wffgPnpT4j6m/A+/a2jqgAwf00Er8LsBx4fkQpveA0LzMu1CAph/wMuXRH/L/hg/FdJ7vxyWxEA9b8EBCeA/gY+qYsIXp8bCUUp4wjhCs8HlfKlBXK/lQItT47/9gPt/b8bYP63SrAAA3KoA6NAuwVHzBQGF4G5BAAHgAJUqVUAnvHG/hI9pf2e4dH8Lh6T/IcEI/33hKf+F4CUAMll/QHhCUQCwGRmARAXBwCjVwv/xUMW/5b9HACxsV0BPXxCAmLrjwJU2FYCVE7hAVJb+QDGnD3/iAr3/P+gKvubZJj6lIxr+wrTGf3uU+D+kdYsAJrSjgBdeOz/x1C5/mdBaP34RhH80vII+yfb7/oaoP77lmu4/X51k//dhbcA45PgAC7POgCuLA7/Wt65/Zv/t/yvJ0P8xTtd/BMeRf1oHYz+qUS9/8EKRAAn9rD/Dhwt/uqEq/zHGpv7wu00+1xwgvuhsif83GHN/BSYPP2c43T9cx50/QVhif1XAF/9Mtnb/M1FMvx7A7L7Sh+t++kngfzEMs/9/fKz/vMrGP9BOwH/JuRg/ord4/0kIfL9WU0M/vsZAv7DsPv91qrT/X274f3vDSj+hbzm/RHVCP3m/Bb8oeEY/G9lav6qc94CzKlzB8JJ7QqVSC0MbyvgCv4orQd0aEcD6frh/kVpMvx/yf77QBP+/RLrmQHMBkwFZKngB53i6QhcryMIvDHdBeGQ/ALc2zoAQCm0/tXQOP8xPpMBl5jXBB3XrwfXiPIIWGWXCL9hQQcQKF4FbhtuA//YwQGt9sgAAhjnAC8+BAJvS6gDpeUOBRG7fQWf8qgEoFDiAqqM5QDae1r/s5S1/vb67v7rYIj/7q8TAL6bWAA0HT0AMEL8/xAUm/9/7un+RM4s/i0q+f0p3pz+SrgWAMIA/wFyYJwD0X2CBOd8WgSp6goDZRqhAEx30P33OLv7uB0J+ypPfPsZLHL8U3y7/QnANP993kMA0RU+AAnWUP9OMPn9GfBz/LKANfu6Z/X6QFQB/JqC3f1lQq3/6/i6AGgc/gCZPpQAkZHN/8UHwf6J6HL9tjpP/K9P6/sAAZP882HQ/e98z/5ReyT/0FDr/vyBCf63i+f8MmAp/NQ4/PtyShT8ODV7/PjEMP1XVub927M8/uCj4P21ppn8SwBL+7ByxPqNV+j6HQ2f+wMnsvyL4ID9JcuG/T8c9/yDKoL8e4W+/FEOsfzFk7v7phu5+kt74Pq7jTX8w5r2/ZWNDf9y1D7/v7l1/hg88vwbSL/8TKt2/8r29gO7P9UHasrECQJ9aAlGvLMHWk1FBYjnEAJ4Yg7/fb5w/f/7mP3mb63/a7HDApnHaAUDVP8GCVAOBxtMSwXHr7oCFj9wAH01Kv+AVYz/S2aKAXl7YAQOmAcH7MxaCJ824wdFbH0G1/35BI5k8wP8AHwD/fOMAzHRDgSXB/8EEpD2BU8pTQYKpaQFzRz9AzKG2AGT2AEATCkN/5HjCP95MeT/W+ccAY3sKAK+k34CbfzeAeKWbQCkf8b+66p7/Rkfy/wJGtX88R+o/TAgU/+5FV8BZTb0Aqj4kQOWz2gDXgWcAi2nQwE+PAQASEeD/y9Eg/+5lyn/8/5H/hGQgv2kQGD9lz2g/baerf3KTWT9gsAJ/ckt2vwrwMv8sInr/BHcNf3xm1z9GuMr/ZZTwvxEeZ78gMkl/asYFv5DBAD/ywGG/9GNwP9X+L7/WFOC/3AvIf+jFrL+XX4u/mpfqv0cXkv98TMA/a8E1/z5jez83zo//bY9V/3RYBz98Nes/Kd/ZfzwPDD89S5C/HB9oPxlyNz8mPik/A9PD/zwX5/7DiCn+30eWvzRUij9qNVZ/dLNQf1wNrz9PyYg/tdNE/6a+q79z/Pi/IK49/teDZ37RRbv+zd44fxfLcb9SDWc/UX3rfy8QcL7Vzse/AdBDP9aAGUE/JlECt0/Yg7yVPENM/SCCOe6HQFKRbf7iz/1+vnHZP4J/5UCfZbsBMcDdQXVSGIFl03LBUbB0wWDBesDDsFAAACu6vxCv4H8w2RFAGHZNQZBG4YKxR7hChcFkAc0dzED75ytAHjWBQFXolADJoTeBaiaegfAyOwHAlG0B4qgIgfbvxMGTD9CBOb75AHQlgEApMWF/wx8fAB4HEsCOeTLA3ttwQMh/fEBOyGF/8tT/f2uSNn9PsjL/lXVHQDCBisBn9aHAdysVAGtFAIB3IN3ALOErP/Buwz/M7kt/8UAFQCHMxoB2YunAdwPvQF+W1oBNkMSAGWKE/5yzGv8Y+DN+4+1Gvyt+Mr84OWC/Qkr/v3d4uj9IuE2/Z2xUvze+OD7DYcI/OITvvwZYKH9YjRd/sOY8f7uLAL/nrYp/kK9z/zWafn7o3Q6/DevC/3nQtT9MWGC/jUq3f4kUKn+Lbb9/UKTEv3/NBn8WJFe+zX4QPuJafv7FHU7/R3/Wv5wrW/+oUFq/cMs4Pt7kZ/6Id7a+dFAivnkeSn6xWSa+5qJ0/yP5lr9u7KH/YF0Kv1cTov8T8UG/ABCxvv+MOL7TW5N/BivKvwD7s37aYu+/F0IEgDe7PQEUzdqCEydoAgWx9YGGLUDBVMSfgM3txkCRqqrAPJ51f8d0GgAI31hAitshASN74cFEeF8BIzPrAHw8hP/kLND/rUTTf+57CUB/svwAmHCgQRfS6sF1tvxBST++wQsUTEDb83eAef7FQKb/cwD2l//BR0ukweHP+0H06IhBzR7hgU9VbgDLItsAq2L3gFmOQ0CaSD5AtWOHgRGWKcEOMY9BIY5AAPUC30Bx9JFAKcVtP/O1hEAR1gmAV4SBQIxcjUCGGaxAdxyxgDoIvn/qP2U/6rlpv/Tjf7/LHqaAFKRcQHCQykCWHsOAqCTAgEPEJH/esNj/g438f3deSj+WuK3/guIK/9GcDT/DzzS/vTfLP5Iy7L93aCC/XMFRf2Vusz8sCWI/J3MtPw4bdH8GqqW/H3Db/weHKf8kFcl/f7rw/1dIkz+oVCV/sVoaf773tf9I5Ao/XhxtfyqhNP8HcdA/b1WXf3k02v9AzHU/Z+vIf42xM79mwsk/QXBmPzFwPj78JNh+1xqLftlokH77f9A+17LM/ueWIH72HC/+9FosfsLYJr7gkB/+0hEj/t7owz84ViB/LeQUPyLO937xyOn+xTPn/t8SBT8x7pL/iLL2gKmCoMH84EbCWaSZwcuCq8ELw7OAqgBAgKlS7wBabNBAjJLiQNtBJsE/r1gBBOq7wLdfSkBl2Z+/6GaLf7svun9YKJP/3majgEl5HMDPUgUBMfNogPDeJsC4PeuARGyHAHNFJEBFVxQA/UX8QXKtdoH92VCCJVoGgeefUcFj+lsA7fBWQKK5mcCQ1HkAlY5JgVxUEQFCOYDA7mvXQJ1tcQBCes6/ytHHf/bIOQApFCzAU4MEwKD5EICH2RkAQJT9v+F1C4A/rmy/wNyn/+mURsBf7dNA9OSPgS3zcwD/zgPA4+huwG+i34Aer6c/79gsf/S/B0AVfieAAWB3gALEw8AndPX/m3Xq/4G5bD+HxqE/dwCS/1Jzo79YM5i/cQgnv0xC0D+l5BH/sjTDf4SjjP+Pipm/QzkMv01k+n9ZEp8/lCDGP6vmbb9gOlU/Wcr4fwAlTP9yyGj/fyst/3oMFr9mGve/MbeV/y467H8ZvTo/PQ4lPz0XuP7CZA++2ax/vp71WL7+UFE/ELRyfz64bX8BSSV+ykQM/q00aj59AyS+mvz6/u2+6j8JHI6/NfA3fqjXen5cezJ+eKmYfpkU8T8I1LGASpnoQa/TO4H387LBU9cXwMSEt8CUyiMA8hU3QMVDdkDF0FnBAOiEAXRWQ4F/3AFBHpfUgL+WXUAGi/a/iCEU/5WFm7/hJ2fAVvF2ALp9xYCEjg8AIOZ8v4PL8v+8B5p/xAjhgDQMAICf+LVA+3eFQUKKE8F07bVBJZLUwRwtxIEqBwbBHn3ZgQfi8wEjm3pBPWRUgS1gFEDIzNTAltvgwFAh98AkK/HAJKyIAGaYVABuXK4ANWRqf8mZBz/rOAp/7yMJf+log//bFav/4ll0QBa35gBm2quAcOioQHG1YsBxGMdAbGGpQBoeeMA2QTZAY+4iwJC5mcCkn+9Abh2QAFfvu8AE5+FAFPHIgA25ND/t6t6/8OEIf/0yPP+Fa+8/gqyNf6wPsT94rTV/e/6Bv6D1/394CvZ/Tunz/2YUtj9pMiA/Uz18fyriKT8n8rD/AVOSP3Aivr9oXRL/jrWJv43xdj9l1J3/WQaN/02RID9k7YS/s1RYv5KFcn9Z7uj/FdFRfwh/Mv8YS5U/bIY/vw7k1v85RcC/HShjfta+8H62NFm+r4s2fpGQ2z7Xuxn+2xG+fqiFaD71lp+/tqsmAL3YuEEDu2/A8vGfgGzeCoB1U/iAuyszwQmgewFzmwpBqHmrgUhzIIEZ18gAxcKWgKIRz8CAbEFAus1WAGn1C0By5WcAR1uyQFtwh0BHXAHAG18U//3DW3/pa/t/+fPaAAbPBEB94u8AfIYXgL407ICJXKjAokSOALF/w0CC5GBArHENgNmQZgD4oQ2A0TYgwJvxhMCihkSAjlKCwJZHxkCD4IgAvbiwQGQGRABVtMsAHZ8gP+8xmv/ChbO/787/f+dkMf/nG5o/8pQIP8YmTX/Hp2y/+3/AACKwpr/IDov/2NZSP+LDcP/XQMFABdlCwCGkgYAZqfQ/6ocoP9hubX/PCY/AFvnqwCXJ5AAS2wHAOEegP/K2EX/7W5Y/7Thfv9YqW7/vRAh/yuVn/40UBL+jZTc/ZF2Cv7XOTD+wIvo/UTmgP0nZG793453/TXGcP0rSV79Atxx/Sn4fv33Q1b9P26D/Z9g8P1bPub9K2pc/Vak3fyLnxH9FPF+/ZU/nv2SRqn9LL/M/f6A//1qp/b9yvex/S7lbf3LGw79r7Fu/DyYN/1PiOQAgpKDBfU0wAZCUY0D4fEMAFybIQC/cOICXwHzBCCITAVMJ9oESF9OBMHKXgOQSVMCrC4AAm6PaQLZun4CWOPqAc8HwAGPUyICDInfARVUVgDPLbr+7iuQ/vr38f84yF8BOUDLAXshxwFYmdwBtdjeAf6NlAHxtE0B3SV4Ac4K6QHARQ8CLcuxAUaSSAHTkAkBsTnxAAEpxwBaCbYAdjDNADbxWQBbXjH/9dFK/tMygv56hyz/jxYn/8uHbf5ndxX+Nx9l/hnLoP4PA3r+AMSu/onmbf+ncNf/ILnF/zHnnv8cTsz/J5NBAFKCdQChqT8AwooFABrR0/8hHWz/V87k/vGKSv7j2gL+8Ys0/iEUof4zzgP/lgl5/6dY1f+Wk43/hMLe/hAHd/59EKb+73Lf/hEl9P4stO3+H1Zz/jgLXP0rsqj8ztJd/dHsbf7HIF3+sON//UevK/0Np3z9SgS8/fxp8v1J7UT+9WFW/p5U7f2U4un9EnXC/rtMWf/p3N3+j3Xm/aXyaP2k6wv+lPg8/4EAo//ix93++/rE/dkhPv69REABrPPUBDsJYAVQN+QCbWZcASDQIwPNq/cF0B7XBqfa7AWyZWUFP7XtBRAWAAYXih0F1FZRBKuPCwQCOIQDQEyyAs45jwIQKzEDrMMCA0pkLwH/U1f/hkVw/ypk5QAQ7bwBHRtNAXE8iwDioHQA6IHJABE33wAqL+AAt5g2AVmOhQEC0jcBT+7mAKk5FAFsR3wBoy6aATdO+QCf/SUATOmj/9x3iP82IGD/1Wq6/h0p7v3Af379uiRh/RQVgP1Qu4H9lXl1/aJsT/0FCsz8qatv/OHuyvwsHbT98SQu/hX+Dv5AGR7+Zeuw/iX7If8KfyH/Eefp/ukDzP74Icj+tE+n/gPhnP4DQMj+gov0/vFtvP4ZU0r+TVs6/rufi/63/ZD+lf0n/lEq7P0o7FL+0yES/5xZTf8B7ar+ny69/TFZGv1g+Qj9XIGe/SbIGf7k3CL+o22i/T0lp/xi0iP8ONaQ/Dycov1SP1v+dxUK/jMiVf3xnzv95iTZ/RH2hv6FHcD+DL+K/mIjWv6COkf+r/h8/v+4/P5Goor/8haQALUiawIvvhQE7i0QBLJ/9wLCquUCVEGcBN3dBAYDWOYFF2tgBeZ3fgUufrEFC1NHBdsm7AT3ozsFqDOXBY04FQWqsU4ERAokBK1BRgTPOZ8D/GpdApLe/wHis+UCMR+0Aw9gPAMoAAYCwPV4AQTbmAFf664BRmq/ASbLAwIJjlsCwuU2AkTMjAHarx0Bk94cAXH0HQEbKhYBupA1ARmyVgHDTxIB48EdAJ/KTf+7MF//JYBa/x/Y1v5UJSz+lcuc/ZuQPv1d2PH8LRGz/M1g0Pzh+uT8g4PD/BAtmfznOGn8aaaS/BKyH/1Ynsn9Bo0j/tLiE/6KVb79Dex2/Xx9Wv3O1MH9N314/krR2/6UUYv+tMz//bEM//0z2zX+LHT6/V8dpP1I1M79sawc/rxpAf6RLGX9inTa/JomxvwqNBn9l5Rn/fUHUf0DrxP9EhQQ/btMzPzPWD38d7oH/K1gVfzCULj8n+q8/Amri/yru6r87FHU/Laz3PwtSM78JcLS/IiuQf1Rc5L9vb/B/QvoHv+ro6IB1HQ2Ay9qCwObMCgCroltAvrArANuFboEZdhsBZjCXAZZitAGMO8uBp69iwRiUwkEHdzwBQS8UgZnfH0F/GQXBab4WAReTkIDElRBAil6bQLbWywD3nt6A5N2uAIyTzwB62ggAabdswHfxw8CpArIAu5OcQMRjmUDD6FCA5SriQLzvrYBpWcfAqWBkAJMCSMDNlw+A6iwkQKRH0wCj1neAfMSDQGIdhoBIBVSAfQxEQH85pQAWNbT/5FFI//Fh53+iwOi/rpPoP76yZT+frav/p2FTf68Ctn91Djw/cjaTP75GVT+tq9P/nunIv65ALj9rCdf/R80J/1p82H9JRyJ/VSklf0Eg979Zl2b/e8yLv24+Sz9qfg7/Ws+p/0Hegr+SD0N/r3z0P3wKX79FTo3/XRJ/fyuZub8kPUW/aGSo/2+ft/9enuN/fFzDf0YLHn8GgZ6/Jd23Pwwdcr8XOFR/IJxs/uhxnb7Y3Cz++/dTPzHseb8KO4M/YTSsPwT/mb8BOCV/FBgZf0HhDv+oLHS/hTp5v+WIIABF+DdAgMPHgOpIWYCmBWOATHm5gFTOpsDWyIGBmoWXAeuxWIGCEc9BK47wwLYcZADCiO2BSVg6wbDIUMGOJBJBKIYcAIxEtoBNduOAvhK3wNAJI8EjlTqAyiJkgKrIqsBDs25AbHhUQJ0LPMCEuxSA1YgcANdhgsDRIQlAgt1awGJe1IB/6e/AbBKAAKwd90BmvN7AQEc/ADed5AAzy2JANeq8ADM8FYBnCUKAYbtFQC83TP/ksjm/uU3If/qiYf/u67T/4s/6/99f7D/gD8Q/+xuj/6LwJ3+0DEA/3oD//6BFZz+rx9O/sLSeP4l48v+ky2S/vLp7v2zC3j9Y0eW/dYm8v1CJAD+z+Cr/Ut9OP04QuH8fPSs/DVu0vya/U79l1mq/bvAJ/14sl/8vUxn/Jk3Iv2mvdb9Ft7E/SgvlP1AfqT9QQ+x/dIkrP3+FqL9roif/eV+t/1vgNn9cuS0/f/Rfv1vfXL95ni5/f0NEP5PrzX+T2gn/qEPEP6xF+X9pJS6/fUV8v3eFar+Ya3b/5fD8QDYMU8BEZ4HAaFN/AA9X5UBc4t6AqToPANMxusD/QWXBHvsygQAGlUE8SXnA6sPLQQqecMEAjcCBUiLlQTDMtgDUPxTA67VAgPMFdoCY2QCA94/SQMFZDcDUVSaAj243AEGAJsBS1/ZASIHGQIWrgkCiae0ARDeVgHb8/8AT4rcAEjxFQH7RYEBaW6/AcmnswGfsFUBSunfAHVPmAByN24AefCgAGTE0gAvM80ATMB4AIZyxv9JmyX/WP0E/4vdQv82QXT/rmk4/1BFt/4yy1f+W8sw/tptcP5qcKn+ejF8/hPoJP4owfz9bXTl/eAIzv3pNaj9IyaT/aGrqP3hqN793wgh/hoLNP47YQD+aguW/Q7LDf3sfsb8lXkA/cMJe/0f+Jf9Gr5D/Ut1E/15DiD9NRFf/Vnpi/3wnHH9lfMr/ZwlCf3f0hr9Diab/c9wJf7BXh7+2aSm/Yhod/3k0Lb9s5gi/sM3bv4V4nD+oFiV/v8Sxv4DktD+7yfs/gwKEv9T4kX/aeI2ALdjegFk+DICudvfAfsVZwGsIdMBZKn+As3Q5gNbyzME5T5RBEaIXgS4d1IELzQtBG2xcQTR0fAEKegEBcBXRQRA+3kDjrBPA6VPjAP1VaIDe4l5A06XKwMoCboCnMMfAuDneQFIglwBtY2/AZBjIgKtL/0BzPxuAVI4+gBAvrUA8UetAFA++wA0v0sBsScSAX0abQCGaez/q0Px/w9HYwBuspMArzglANfEn//MPHX/BO1Y/0MqBf96gMv+9l/O/ljkqP4B+0P+GTMd/rivWv52pob+nPpV/lufBf4k7Oj955z4/RmpCv6qFbb98fNF/WUoSP3MOpf96L6x/UQopv1Ruof9wW0b/fPjqPw3OX/8APyw/D7XGv0Thnz9P1hu/WyTAf2F09v83FUe/ZexGf2E+9n8Dk72/IB/if1eNPP9H3jl/QuWn/38wnP9o0Ck/dKRG/6cXVT+11wk/qsp1P0B/ZL9kAbK/YnNmP6+sZn/GNLx/1zf/f8LcH4A/y47Ads9ogEKi6kBIK4UAkGwSAPmJI4EFDf2BHqrywRQtoYEh/mMBMXM7QQXGGsFNBYoBlRFewaYIKMFhkxFBP9IoAO8NvkDKaSJBBT4cgSZZK4DCqzlAllGbQLErUgCciZiAkszoQI4gMgCYvNgAlUHnQGpnwYB3iAkAeGUjQET380BPqnLAVdwiAE2PFUBfz/2APH6iwCXLnsA9m/XACt+JwF0lsQAE8i0/09uHv9qei7/hHs7//kGBP87tuH+HWr5/t9o1f7W3Fr+HGTk/RuID/4nSpr+ooyn/poEF/4gZK/9r060/VCqwv27xJj9sJeV/eXSx/2OU5n9g6kD/TsuhPyZ8HP8XeW0/J9JyfxXun/8KGs6/CxyL/wydvn7kuSd+4VhhfusWtv7djcv/LH+Kfx4AM77qP9u+wK4nvu6vkf8pQcW/Ylrrv1Ra+b9WJGs/euRWf3l4m391G4U/jne6v6sR1//A4VE/3EnHf9aD13/zHv5/5EpDQEq51YC+OYDA3QEwwIJhBkCmQPxAei5+AJ887AE2hH7Ba7Q8gUBdc0E4MbBA9Jq4gMW0iMFjR1tBq55rQbOhKYFyX86BC02bgMu3aoDjylwBJNVxAQQ6CsEWWP/Av6jEAL0vc0BAhcUAq1zdAKfVYQCE7sTAvwEbAHje+4AntzcAB3nLgEjNo4B7k2gAUHSRAFBq8UAk7qWACbdsgCmP80As/vEAF5vsADkVJQA44BTAM1/sv+7VOv+pD+S/ktLrP6Xeuf+E03G/uVuZv7Mjv79OHSS/VoRWv1hY6D9KlET/t56Gv7nzHP9T1TC/DUgw/xB70L9brCz/enHmv1NXRn988S3/NMRp/xIPLP8pDzS/BYi9PxL8+D8vpdp/MEC2vtxa6b7we7R+7F6/PtJYfD7/+7G+10drPvU85n7F1CG+3y1oftRtxb8NGao/D+tyPwu8478ch6M/DHU6Pwom039/EHW/ZQhB/8wSJ0AiwmEAULkFAFwujIAM6ZiANk2CAKlBuoDI7zzBPOA7AQZXWIEp5HjA1lN3QNlMsoENTU2BpgQ+QZtMSMGmgCGBOiHkQOzCfkD1RocBUZXmwUNbToF57dZBEGwYgMiidYCr4MLAz2FywPrRD0E6PS9A9jkhwKe8ZcBzFVnASD+pwHyOvIBI9sFAppiyQFeykEB19WRAOp8/f8pyeD/7L80AFdniwA2u1kARsXE/7yTPP+Qjez+yQvM/m3Z4f4Ukz7/+ceG/0tJMP/RmIf+6bhZ/sNyz/75+zj/y3kA/78kc/7oaRT+lLcd/nc2Wv6K8HH+s59P/jtMEv6G7ar9Zns//ZDbC/29Kyj99tQl/RPfx/yinDr8RjnH+xl5t/uFZNf7AG/4+yVV9fu3K7n7ycx9+/bDd/s+DKz73GgS/LFEWvxwPEf86R4K/GXe+/snjkz8EVnK/OGHPv1SW6n95Pb4/RhY3f6Ex70AyrZJAoLApQFvhF3/c1p5/gTQGQGSnUIFqBj3Bgrt9ASfhOoB/JhJAcNkSANLGQMGzfSnB3dsWwdxEBUFNENVAj+EkgHQCbEDoUWVBk9UJQewTtAEpIvpAXg19QDQ/xYCVgiQA0v7EgQh77kD2qDNAvLijAFr4MQAXr9FAbC3pwJlh3QDrXnTAsgvmQFYau4AFabuANjcLgGsO3wB3V3qARCszwEzPLYAaLg//0gNjf7DhQr/BJTc/2mf/P81MkD/9jhS/oZ2lv1BPCn9axZZ/Wk7O/63jvn+WlCq/lKMdv3tb4f8F3TG/KDCwf1UeG/+D6VP/sdss/2KBSv9rH3P/KG1rfzYAwL9HH6R/TSnnf1aAtn8ncf6+17M5fsPrV78hdG4/GWru/y/FHL8haY1/AK27ftZz5j7sYKS+5naBvwmnIT8EYmI/E3AYvxnBM78UfWn/UHvfP5FQXH/RIY3AAyKJwAt3S//z5fb/kSNrQBseacDubgcBUIn9wPtiMYBV3E5ATCr2wJEZ+8E7rsJBqHG2QVSjqME/jTnAkUADgLxHxUDGMUZBToo9gV4C6kEKz+OAivVqwFz2WkCMwGWA6INEARuicwDSHY4A3zTZgK7JYwBnMV2AWn3TwLOBSQDvKX2AkiICAK3F2MB6aApAbC4HAEI2y4Bbhx+ASflrgFmm0sB5RmeAIx4GgB9Q/P/jeAmAKuRZQBanjwAkADg/zl0hv/RbRT/NgCh/sSdkP6XBun+ztwU/xwUtf6OBwj+V3+E/crQWf1DOnP9CzWE/aajXP2JExf9PSC1/NNWJ/wpneP7qAUT/MfIWPxsdlb8cQY1/DAFHPyZk9r7/TGT+7sNh/sRBr77Gl4M/OLMb/xYq5n8WQx0/J7yN/yKX1z8W1WC/cMLUP/BcE4AHble/+gchP0JO0X9OiG+/0QJzgITZKMDraoAAlx/EgAdBtL/QmY9AUN5MwOADYwEmdR6BKFv7gIDhysBG+L7AHFlvwKsh78ETvHoBLkdJQOPDlUB0A8QAXlSAQL/RfcCejpGA2AXCwOB5nUCr/u1AVnjZAET8MMBBYl8AgqIxwLtJToCPvaLASwVjQHzEBcC1s1DAgsaxgGR21cBtcuoASS9PgKIJDMCpYh3AYMKswCIP2wA35OOACviAwFtH3QB1FtKAb+1SwBuERv/RjvU/iqCvP+Mg8wAONfbALttyP9gg3X++aDu/YLkW/47dwj/Fr81/0PFxv43uu79jTEi/SZG1/wbzDv9KZ/J/TYdw/2w7Rj9Mepf/PjFJPztKVb8BSya/MOLvfzrPsn8M4i6/HWxkvwrQpr8XJEk/d715P2njjb+Bz/6/XFm8f1TT47+S15t//Ng9P9WAAUApQYbAJylcgC5bOMA44UrAQNLXwEoCo4BXW6YAc0HewFxDXwBPlbPAe46EwKGOP8B8MeqASA9ewEI0qoBsk3YAVbvswEwB28BUuxJAXLnTQHePVQBuLdOAWnWVQE9DG0BJq6EASKTggGiTpUBTrS+Aeph1QHJrKsB2QRvAS6TbwG7c5cBqAq5ATRpigHf7xwBaIrJAPJowADdLMQAnACNAGdo+P8rNlz/fCUF/8uxyP4bDJn+H3th/hQwC/72Z5H9OQkY/eZq2vxDQdv8Rizx/Lo8Af1IgSb9hOSK/Xof1P1y/sn9ycC+/d2NQv6/0E3/43MLALBl7P+kFHv/9LOs/+84qQAaGcIBViQ4Am2MDAKV05wBgd9SAZntfwH/DCAC4vHRAh9m8wLHmywCAXIcAZpfogD6ZfUAP3qBAeAlmgGCWTIBlIKiAAMqHQCzl8H/IdWr/2hAxP+wDNb/jkKf/5GyPP9cn+f+FqLl/lPxDv8soBf/HQIP/wuhDv8ftSH/rmop/7g4Lf+u9j3/zVpd/+DWff8B07H/Uffq/3ovAwBacg8ApSYXAPL2PQCl0ZcAZkMKAQb1VwFDpWUBcBA+AUN0FQHQQiABh5lnAbQTygHALu8BlpK8AWILWAHEzx8B4KcaAUHQEQFIFtgApoeRACZ6hgDyto8AYUhAAEofhP+4EwD/8UYh/09ogP/bu4X/K1MK/zkvXv6IAOz94OHs/RH+Q/7sHI7+NCGS/poBMf5516r9YAN5/ft30f1ae2L+VdKy/rkVoP7yiV/+8jo7/kpaWv5G7sL+VrpR/1CW2P/1mAcAf6nN/+Lubf9/5Wr/LWkKAMIl3ACBST8BbmAGAe8KiwA9KT8Axt9vAI117gCBZlMBd29UAdq23gAPd1cAoGUgAKAEVgCpZqoAyuDKAGmykgB5tCkAo0Xf//8I2f8HVxAAZm1UACswcABz81QAqmMmAMpO/P9ZeP3/7WEhAMlVTQCVUXMAR+2DAOxbZwDzB04Agh1KAMEkPgDBbhwAYhUBANM++f/kVtD/QCeH/yBYLv8bU/D+tSPP/sH0vf6vVqf++Z1x/oitIv5vBNn9NyG+/Seh5v3NS1L+bLDF/uVOKP+/V5b/JNXf/+FZsf/vIyL/k+f//mP09f/vT6EBdAjDAgYtewK7okQByLZfAAkDpgAMv9oB/s4YA66qhQNR+7sCFk89ARNWKABtP1AADaRwAbOoXwKbsS4Cc54OAa4o+/9IJK//KFYRABjNlgBd488Ay1qbAMiBGAD+bov/KCxl/8C1qf+UD/z/lvIBAFUZyf+Znab/5K+x/3GDuv/BPJ7/hcd1/70iev9jObT/d5b1/+eSGwAx6QYAhrm8/yl1ef//iIv/uPv3/8TZdwDTtZ0AXIhcAISM/v+jXuP/AI4dAPKOgwAqPt4AnJz1AJbgxQAJo3YAll9KAIYRWwAvVJkAFhXKAHk7wAB3nX0AFvFLADLrPAAXJUUAxn9PANBYRgAJ5BsAM/Lc/2YEvf/gYs3/Ubzg//mUt/824Gb/+WQo/0l7Lf9yG3D/ryTK/6Fj1/8FZ0D/ySJk/q4wJ/6krb3+ei+Q/+g6xP/PUDz/uhmF/hP0FP6QVRz+mFt7/tQxAP/Aoiv/eW6r/lyry/0C1kX9fjZ2/bbMA/7kXWr+lOCa/l52w/5USvj+2BMg/64WEv9DN+D+ItPr/twFb/99PFMAqyAtAcpThgHZ6y8Bxn9wAOJMGQCrT60AcSXdAS13zwLJgdQCpi3+AYsWDwGIDcQAm19EAc/cGgLMcJACqXRFAjhzegEd3b4AvWiTAOkr9wBojH8BLautAVC9aAF0EOwAdziPALVefgA6SKIAyV7TAAID4gBig8YAv2yJAEl2VABqtT8AVp8/ADk1NQCjHRwA/YwTAGrFJgAQfjQArVMNAKokxP8g3Zf/4rCo/wpa7P/1VzQACJNZALoXRQCvMg0AL33i/7wr4/8MIRQAWBVPAOIDbwB2SF4AY08uAFIOAQDvnd7/JQfI/5Rav//lCcP/nPLR//Tm0f/fBp//uEo+/wnb6/6w+9r+6SoI/7t7TP+HCW//IipM/w5I8f4n8a7+S/G5/iopAv9QTE//TQV5/9WMgP/w8Wj/ihM8/9rvJf8T2k3/2YWl/4Ec7f8nh/H/TLne/63Q1/82aNX/GeDK/9IxyP+ANN3/dLz+/wYxBQDM3t7/NUaY/31GZf9kM2n/b2uH/66Ppf+nhZn/KFxf/3ALDf+ep9f+KJ7U/vJhAv9Ki17/5TS5/yyB6/+MP+//7nbW/3mKxf+DJ9P/xXopAJnlwgDNiFEB5WiMAbMAXQHCpxoB3RQeAW12dwGr1usBDUc3Am1FPAKVwgYCf8e6AfCWggETR4QBYD61ASwA5gFzUM8BU4ZYAc7P1gDdeZ0AOUKkAHqTrwB4GIsARmBEAEdF+v8PVrX/qj1//0uwU/9reT3/9PYn/6fl//55YM7+iIyn/qLlmv7kY5X+UsON/qrngv6vd3/+aoKG/iMdoP50N87+wPP6/r9PFv9tayL/B7Eu/+F/Vf+VQZz/tWHd/+Cq9v/hKPv/H5wRADafPgCtImoAyy2FAI2elQBcrJsArVKaAD4inAB9C7EAJAbXAKzd5QD8uNAAgE2iAFjCggBanoYAIAuZAJRLoADARoQA00FNANyvCwBZHuj/rizq//5EAABn2P3/eebF//tCc/+78zf/+0ox/zUNOP/FnCD/6L3u/iN2uP7hbof+ttVg/hpHTf7L4Fr+YlWI/uhLFP++o1wAqY29AUFNnwE6M1f/0BjN/NwY+PyuB2EAC28aBJeVuAQo2goC33jr/m9TI/7GKub/b4V0AjlO+APQbJoDuem5AY82o//rpOv+WG8JANKTuwH88TgCmLAXAWeSpP+lDD//+pbW/zrNVwCx0B0AGbN9/wZYDv9gUA3/VzRx/xBb/v8MJDYAItCq/3ATmP7nB/f9yiWg/qe3LgBWtDEBJem7ANxVSv9HDTv+MIJh/i4PdP+A14AADFjGALDlMQAmSDz/HCOe/iFryv7LvZT/KRVBACzsRABS6bz/NERE/586QP/GyXn/+rSV/83iff/o7m//LquQ/7J1vP9RZdj/qYHR//Resf8exoz/HTyL/w2gyP/JejUAbgqNAI/lgQDEnSIAy17Q/6uC5/+0yVsAxN3UALD+/gBQPtEApRd/APy6TwC9oGgAJbe5AK63+QA27/UAPmS3AFfceQB2W24ASBeEAPpqmQB+BIsAndRqAMyhWwAdYWsAxfSKAGR9kQD68nkAtxhNAG44IAAd2RIAZcsvAJ3sWwDXrmYAxOU1AF9q6f/A67b//Ea8/6FV8P93rBIANx8HADjA2P/496n/JAqO/ynBl/+AA7r/1ZbQ/90ayf8x06L/T4SE/8zfev9VrYv/MSGe/6zWpf8I/aL/NLOh/1Wosv8MGMb/rezL/3m4tf9+oI//oax1/yf3dP/abov//4Cn/y2Zpf9kNYb/E+to/6V3Vf9R1lr/c/R6/1+9kf9fxIb/Mh1d/0NkQv+b10z/VQ12/xCMnv/XOZz/TASC/3iKcv9Z7IX/GYiz/5aa2P9B897/47PO/9QBvP+Iu8D/zFHe/9P8BQC7mygADwkvAHI7IwBPDQ8AXVIJADjWGgDr4zAAg9I+AAFlNwDC1iMAP6wXAD6oHACahSwA42s4APLpNwDtgCQAsYULANC5AADzLggA/1sZAP2aHwDgJB4APAobAP7VGQCxYxYA2coXAO/rKgCv1EcAqxxcAGusXgBaY2cAmbuCAHQXqQDlIrwAMwa4AFtKsQBbUrYAKIfLAC2i1QBhjNIA1KHLAExnvADkfKkAoY+PALXDggDu/oYAZhOBALffZACc2DIARN4UABhjDgCm5gQA9qD4/8zt5v9l6db/b73A/8RApf/8SI7/m5+D/xpVjf/mUJ//T46o/609of853pX/cKqH/2g1jv/L6LD/AVTn//1PEQAH7QgAxv3z/ypr8/+qKRoAB3pPAGWQbQDVVnYAXMp7AOpWiQBvkJkAkmCmAJc1sQADrK8AkPmdABGNgADqOmsAGQxwALt9dwDMt2wAtGZEAHncFQCtjff/kLnv/0JL8/9mG/D/QAnm/2ghzv98vq//efqX/66hjf9rFJj/5rqi/6uipf9Ahqn/Tc6z/5Nhwf/tUsT/jgnE/0C8y//1w+L/bKj6/4EMBQCAMvz/vlTy/9Yz+P+JhBAAQnkyAFIRRgAFXDsAnFgcAMYfBACRjv//16wKAF+YFgDThCIAHb4eANHvDgA4cvr/GuPm/0GU4/9+UPb/3PweAFtZPgA/LDgAYCUIAH9xzf87Nb//f2Xk/5W3GQBmCzEAma8aAFEE8f+5fM//Mi7F/4zn0v8NUvH/sUAIAB6lAQBMMNz/GQa//5G2vP+Oj8z/o/zV/4Xyy/94hsf/G8PP/1zF2v80FNb/PU3F//HUt//W57D/mIqy/+mauv/Gz8b/2SLQ/1ZBzP/RPLb/Qrij/9Fxr/+tPND/RNz4/3RrEQBuIBMAP6z8/9Ke3P/e2tj/qTn4/zXJJQDoGTsAJJMuAKoqJAAPsC8A1aY9AJubOACrZiQAtSIfAMcAMQCUKDwAxxc1APvZKABgIBoAYzEJAMBJ+P+onPf/HBELADU1IACGrBoAJNP8/7Uj3v/Nd8//I1nN/xScyv8h4tT/UcPs/weB/P8QF/r/LLHo/wz42P9Wstv/YsPp/2RpAACX9SMAKH1AAALSNADEjAQAQ1ri/7Mq/v8neDoAOGJoAHJsbQDy6FIA2is5ANBWLAAxYC0ASM43AJK3QgD7m0wA4blIACvBMwDBeiIAoqsWALcRDgCVJg0AOu8TAIV1DQDZKvj/5k/c/8qV0f9S3tn///rZ/1VFzv+Q4sH/SMXA/yrY0f+WI9//la7S//zzqf/K7Y//gUOm/2Fy1v81QPT/Fgrg/7c2vv+FtrH/vPrH/6K/6f8QxAMAemcSAG9cDAA1m/z/vPfw/yNr/f/H1xoA1JEpAJLvHAC+ZgYAsEwGAD7QFwANMCMA9Y0QAIEU6v899ND/rxTN/3Mo2P8VwuP/7w3c/+vOwf8u95b/FZJ1/x8Dgf8WeLT/bJbt/2iF8v9SE8j/FD+h/+9xtv9G7PX/LwAtAIEmMQBaMxYAc18FABA4EgARakMAHWp5APttoQBUgqEA7Wx/ABIpUgBYy0gA5VNmAHPplAAulJ4AQjpzAIpPLwBoPfP/JPj4/4HNKgDjOU4A1TkxAHxk4P8UQ5//+lie/3UNxv/oeOz/Qvry/4cVz/9UBZj/vhJu/05wfP8zZbX/0Irh/8xc2P8zXKH/hXt9/xaNiv8eEbj/zJjh/wkM7P8vk+T/Jq7X/4kI2P8KX+z/TV8JAJ//IQA66R8A2IoHAA3c9f9fYwAA1ksdAKyvKgCOyBcAH0L6/9lm8v/RKwkAIyUpACSoMgD1GBoAOTj5/0zG6/+EBP7/4gwhAMrZLACcuR8AY3wFAMme8f8Ye/P/+RkJAOU6JACXbywA67kbAAXZBAD9nfj/RtL6/69bAwB6mAgAk/AJAJvkBQDK7AEA1FIDAJmeBAA8bAMAeFj6/0M37P8RLvH/eroCAEQIEgCy4AoAH9zv/5++3v/haOr/zJ79/xv0BwDF5gUA/5P//+1vAgD0ygEAzGoDAHgkCgDnLxYA65EfAEgmEwAB2vv/nZvq/zPC6f9N+f3/RDYJACJgGAC3bwsAh4Lt/0Sm3P8Pd97/W7Ly/xVi7f88R9n/9hDJ/0Ae1P+kB+n/cwft/5Op2v9rxL//eTO0/1WxwP8zCNr/LZ/w/6Io9P8qcuP/zE3O/+sfyv+nmtv/3gP8/2dZEwD9+xgAx1QQADe3AADe1Pb/uU39/5NXHwDHc0UAVdtPAIU7OgDZrCkAO0ovAKv+OwAzSzEAuF1WAHkp/AAhAbUBioGUAVCccABLO2H/ht9r/zgDSgBiuvYAQfcSAWgF6wDeqZAA0Iji/yerNv8bwCz/xfG2/wWCDQBj+7//oNBU/9GsUv/e3G7/KLEs/4Fbmv5NWFr+OEma/mCtAf/500H/6Clu/9Xqhv+KEVn/M1gR/zVIFf/eXqH/FM5IAIzVgADgXVEApXU5ABK6eQAz3dAAFqj1AH1M6wDki+wAaUYIARj3KwHrqFQBG0x5AabrcgG5CSYBaW+/AFbhogBxI+cAnLc5AXONNwEY1OMAxCeQABmOVwDqei8AKk4UACkPHQBbaDQAVAkcAI9n3v9Ahrv/piDU/5Ub8f/sSsj/IiN0/83LRf9192P/NBCh/2U8tf+l3In/bf4z/zTF1f7fDqb+xK/I/n32Hf+Z0U7/27Ic/9TXsv5fXmP+075X/o9TZ/58p2j+0hJb/kZBVv73dWv+QsKM/lYz1/7PiWr/bUklANnOoQA+9YIAcZvh/zIdWv/8GYr/FBuHADtsvgG0Z2ICAVsRAnWPKgGjNoEATMSjAJiJYwGaYQQC1IkAAkOJcwE/z+oAj3POAHGaAQHHYiAB/eHmACQYaQDY7AEAx579/wy3ZgDXKd8AGt30AClLfQCC8MX/M2xl//XemP+BxygAZcebAG2gogBrjUgAStvT/78Hlv+M3a7/6Dnu/z0qFwAlTwQA7d/f/3+X6P/WpykA/0JhAJehLwCOg6H/B7Iz/9tJXP+c9QAAYWCnANII2gABNoAASnXU/+TdQf8s6yb/AX6H/2e19v/1svX/biOL/0QiHf82Kur+ndvd/ntNuf5Vq37+qOhM/hPbKP7bXRT+6QcQ/pYkJf5OZCr+0v/5/WyFvP3uzNn9oP6P/jCWlv+2dWMA+saZAMBbMwBn1I3/Zhs0/9SYrP852fQAVWtVAujo5QKmcWoCuD6aAQM0QAEqzpgB5f0tAvEkfQJLCmsC5FAoAqJd8AHAZeMB8EnuAfPzygF/3lQB27OxALu2UAAiYYsAPuUgAWxjdwEbpiIBp8dOABOBgf856SP/DJRX/1VY3/8ER2AATcNsAHMy5/9vXUH/1dUM/5NHVv9pPbP/d2S5/45xlv+TS7X/GrgIAKoXMgC87AMA4te//0n2jP9Xy3j/J2yp/6VsQgBbEPEA8izrAIwaAgCPaQj/5GTK/q2CM/9bYpH/9sWU/+pJZ/8g+Cr/puu2/iO7Jf6oVNP9qizG/Stxtf1RF2n9iHs5/at2cP03/879nAfT/TDjdf1OpDP9Rc1C/Weatf2ZXYD+ucCP/y44owAjdQwBmKmYAOuhvv/8Rnb/C0ktAEqxjgHDQ90C1MZeA2YDDANFCVAC4hrSAYJ79AHeLn0CiGPQArtcowKbuEICi/IfAh3GTAJ5R14CAj30AWZZKgHdFm0ASkkmABLlbAAbjgwBQpB2ARhSOgGifVYA7qlQ/8en0v7oZBL/2Jm2/4fjLQAsByYAkNe//yL+Z/8BVU3/yI5i/2ORef8FDIj/2r6S/8+Rnv86XNL/NkZAAGGJmQAU+FoA1Xeo/1nYVP/CWcn/J+qDAMug2QA1F8wAIh+EAIdS9P+QEUr/C4gJ/672cP9C2d7/6kqY/7mTzf5AgE/+Ba9M/k/uNf4mmMj92bVR/QTkJ/3n4yH94mwH/cfA8vxwIfb8jQnr/E/gwfzSi738r1Ie/Qb0v/0e2jT+3CV2/qmi5v4of7X/gvqaAKOmJgEnIjUBArAUAXa7MQFkDL4B6MujAl2DiQNgq/kDfq7TA4eXaAMt6ikDkENYA6zuqgP8uLgDq+p7A6OOHAO+b8ACbCt6AlIJOgKpC+cBk/yHASECCQHT5IwAVD9dABxFbwAhf4cAVvdIAMxLtf8b7h//lMfZ/ofR5/46TBv/H5dI/9a9UP/lwTD/GJgX/xqLUf/+oMH/Ahr8/7Xtvv9LkXX/dHia/6mmFABt0nAA7D+IAPkfngD2gZkAtl9sAF8MQgDso0QAq5NpAEetYQBAhBkAjkfS/9WcvP8ujY3/NWgO/3+TaP4eLej941K8/Up9ov3X3nr9/rFO/cdaFP2wPYn87FHr+89Gs/s+3cL7DaH++3RxGPysMDv8GOyT/Lwq5vwThBT9B/lJ/fy+m/24s+L9BwdI/umM4v6WBLz/DZXCAIIpfQGrzcEBUNzfAT96HAIXqo8C6EYdA3F4ogMVAAYErlRZBFGmigQ6ZpwE1l2mBAV6hQReeTUE9u7pA3HBxgNZns4DG7vUA/MGowNV/TADJvWgAmDSGgJ1P64Bthl4AVHmXwGtKDoBeFbtABIGhwAtAiIAIM/b/yWOq/9CKnf/6qJY/4IMSv9IfD3/fNUy/07MJv8N2QT/0//R/hWno/4eXY3+Dg6q/smc4f5qXP7+TwPq/uTIqv7JLGD+Sm00/paOLf5iUy7+zqpF/go1S/6L/BL+vwjM/ey7lf1+f4D9eaB1/SmnTv0QNxz9xegI/UnoFf3jnSn9Nv4+/ajmRf2W7zz9tupb/QXZnv3GLPf95QlX/jomrv48+fj+bttL/3HxnP/SoPL/kGVpACkO5ACpkl8BdyPWAe92IQJT1kQCCC9iAltvgwLM2q4CiH3iAsVY6gIqJtkCUkjuAltBBwPbpRgDODUDA+4kzALd5ZACbCJmAi0QIgIB+dkBeyW8AezDmQFWz2UBt1UbAVlV1QB8+KkAVq+FALQcPwAfZv//oefm//K+1f98bMn/E/W5/wREsv8KgZ7/mXiH/ybAgP9tY5H/A1LS/75e+f9fGwUAgrANAOaODABaUg0AhaMfAAS1SgCLx2gAOblqACM3WQDwDjYAunsMADDBzf/O7ZD/Bv9n/9t3Ef8WwKX+zzBl/ukJRf5ud+T9+Fsz/UPXuvzh34P8DhVl/I17HPyfZsn7oBW/+5SLqPsDs3z7MRpU+6Wwg/vyGdX7THoQ/CBSUvwjGID8oCry/KybZf1lJMv9jxtS/n+5F/+ubPj/rWS8AAzQhQHeiz0CDMIZA1mmxgNZLwcEi8ohBCQEVAQvOc8EoKqRBRCncQYJ1PMG1f/3BnYwowZY+xsG80LJBYzjqQWf8ogFrsNUBUfZCwURx7MEahBCBDiQvQO9MwwD9SJZAj8nuwGKKCoBWtLWALgruQD+p7AAX7CBAEWvGgA4AJD/Aln//v5btP5Z26j+L5bW/jDqGv+LXjL/MUEx/xlgH/9Zix3/D8cz/0UNXP8h05D/aDC4//tI7f98WQ8AkaMKAGYx7f8/aMv/Bsi0/7Fnof8wJpP/d4+I/5sVfP89PCT/eEef/tp2Lv6Oqcr9onVV/baPxPxmvlP8bdb9++OHzPsTB5j7zKcx+5lfnPpaReX5JkY4+TV7zfjU2oH4v4F6+EkxrfjYM6/43WuK+KLtUfhc81v4T1aF+BsF3vhq7WH5xgT8+WzWpvryAxH7xGKJ+4BBTvw2ek79bk55/i7gjP/C750AKeX1Aer7aAOOWkwEIzlyBOQVegRsswkFCbM+Bl0DnQcRLH8I0zTmCBuIDQn3H+4IVffRCDEjtwgL0YsIpOdCCGXxCAiKmekHI/nXB2RWrgfKexUHiuMmBsxMFAVu3RkESEBzAy3/FwM1beUCA/6ZAq73/gFmvhsBOTk1AByrg/9kXgX/Atem/nDAi/6eorD+Vz+6/odllP5G4X7+LF6L/uXUav6ERhj+RLrz/UxZgv5oR2H/sLbz/4p+MQCKsncAktamAKOahQA0b2YAghK6AHfNVQHr8XkBTD02ASK2PAGTyZUB9NeCAWZ3AgEOb1cAvX/d//uImP/e+D//Fvj4/o5Ku/6CwWn+8Tyd/fzHY/w2p5P7ay85+4zaDfsHNIH6QjP++UIR6vnYMrL52/MI+SQyGvgjaeb3MND/9/T3o/eQ5j/33nRw93iN5/e/e/r3XBPJ9+780vcPBVT4vK7n+DWxMPml0Sz6zWmS++WSs/yn6hf9q4Xm/Av1ov3G9cr+MTeS/+bfAQAsl1wBNsJlA0DWlgTGWnoFGg1UBiRYIwdbRlcH7rCPBjusgQbD5Z0HIDUYCViiywkHNAUKtcFUCk/oAwrdVWEJ6oSeCEohUggowZwIfapvCFBA6wdCBK0HmUu+B3/mVwcVoDQGongkBSTyTgRPFdkDbXY1Ay81wwIX6PoCLvnSAsqe+QGZwewAvGQoAAkeyv9f0Zb/yZNZ/+hCGP8xvzL/FUtW/+vVHf9Roun+bpnC/oNwoP4B5GX+NVJU/nE/vv5F53H/dkfF/+P7gP8KyDX/L6QW/3CgDP9zFBn/3t5A/yFRgf9944n/M1tb/w59+P4gXMD+hG6A/oc6+P3h9WL9PusS/bD8Kv23JBD9mI6d/Nb6V/yIv5H8g86o+ypyF/rIdHT6SnMo+wQVI/r4Sir6ftnU+i/IC/rkhn/5aXRB+lV4dPm5P274YCH5+Q4C6PkjUov5H4Iz+ivhhvon1HH7KvlQ+4XSk/q2n2X7cQiB/FhtgPzBNOb8Rjvd/WhQbf5MJs7+PXPZ/qhtYv996ysAUKtlAO98VADXmgoBpxTlAS4z9gEQtI4CTGDtAseplQJhscwCMR9gA0b1SQOpXbcDnhfABHcq6gRF6usEkm6KBaKuZgWauhsFctAjBZdaMQUmj+MF3TRXBrFIhAYGgZ0GUSajBjPLkQYl7iQGNn8FBt/04gXgAdUFebnTBag5cAU6xKwFF861BXLwRAUBg9wEtxknBPYtvwOQ0KoDzS9vA/VtKQNLZg0DZCO/AtAlBAK5L5kB58omAah0mgDT6UIA7ObU/4u8jP+wsW//m9oz/zbP3/6XjUr+28vY/SVUg/2ceA/9urT8/MhA1fw49ab8t1dz/KOiUfy9H3z8t3rp+374Ovu6q2X8RLkP/DiJkPpUbRP86FGR/AIjgPvUntD7dTZB/N3LVPvbz5v7a0K8/MnSXfuASqv8Egp3/pEJTvxtODz8JFAT/1sudfz48TX8aWWJ/xV63fv69gH9yhpIAExgEP333kD9M0LJ/5+MHf3g4fv9QRYd/8xxp/yEVoz+VLTK/rUY4fxiAIj+C4N+/hntb/3f72n+pk61/eKnbf4o2Tz+XXXF/lU+v/4lKtH9ack9/1na+f61kIr+Pd0w/7RP3/+D6Un/89sxAN+YNABbMMD/HfJLAWMORACH4zsAjv7cAUYJAgGrBkQBv9+zAlidowGPtt8BAyuSAydUqQJ3XKUCPJ3JA8C51AOjK7oDMEhHBP1PQAQSuwoEQ8NmBCZcDATC5yQER0erBAhylgQmjUAEvbm6BDaalwRbR3EEoBxlBAcLAQRNmawDhrqZA9+peAPL4jsDorNBAxn0FAOI47YC1ZSLAtokBAJbrgwC0q/5AQbzdQFBWHQBqOxQAdusRwEcDsYAQLl5AIqYiACWtKUAzVCt/nMdlwDQ4In/rEk//hXwcQCUxWD+Eow5/oDPe//wCpD9FMW0/bpW//5x6ZD87D5N/5fhUfxC9LL9zCdc/+yeRfuVbLz+ByJH/Owi/P2iGR/9ZxzO+5EZU/9/U0b8EhG4/GhOUf5NzVD9VwfW/Ed4QP6y34L8b3hf/dv0Xf5g3zb89HI1/j3RWP4cHv77ABCs/SYDgP9EmI37soZF/f+zL/8UofP9UttM/TJqvP4op9b+pv0w/RNAPP/3Xtz9UpFJ/Y62mv8cYnD+TyMe/WC2DQGEUKn92k6W/eUU3wC3Ym39Xu6o/ir0SgDs4Y7/oksT/xrUQgAZblP/JZkRAPz/dwBwX97+Y8bRACn46wBRwUQAK0TTAKmtugERsLUAVlKiAP/ffQGoUfL/G7QNARlwCQJ+zYQBtw33ACZbfALZFK0B+orkAOVzVAKhoqwABlbaAXo3kAJ9Gw4Bb5rOAVNXGgNAEvQB8ZWlAcnBrgJcuPsBwjeWAdkcWwJVSlQCtinEATzcxQJgC20CPtAKAoEwagKOIZsBcrtBAt2KSgLKArEBd9YcAmyv1AIQ5XcClefBAUQzTQOvfIYBYcbiAfM1OgMCNjQB6gijAtm8PAIlRH0CWLswAkmoqAI5NIUBcV/aASYNlwJKruYA1ruCAnLPZwBp3esCfDHXADUcPAByfigDI8M//x0hqgDQBSECWdXg/grUewCkpU0Bt6H5/jIAeADVjGz/edN1/zXMiv/agev+KPhZ/x/ejv5PVpz+BUsF/7UgBf7kAQn+Ryyh/tynCP2eaKD9GOkQ/nBWY/sYsJ3/jPI8/Sfx7PlohTwAcn4U/JVipvtoni7+hXEg/KkEWPwULpL+UVRL+6c3SPwL8Bn/goX1+hGnyP00WRv9GieT/eaHJP3KBJv9nv/B/q01qfxLRBv+nLQ1/9nt5/zkqAD+opEIAVjasfwxD//+NWxfANdP8PwmjHr/8zS/AGIesvz4+xQAdgwRAZTvZPy0WYcAiDoP/2hPFv4wyeD/1SNl/RTLYf+Pnzb+bCfK/tbGGv/6JQT+R9xt/n1L4P7D4UT+mKgW/Tas+P+6L+H94obZ/njuOf8ywOr9/M6PAFXvJv41yd3+z+8rATifhv7lAuD/ZRwuASALHQCgRrUAKY0aAUKFkAG7nvIAS5TBALUZ+QJxFrABCTcHAo5aewN8T0wC8Zz5AmCvugL8Ms4CVV8zBDq1wQLVyoYCFCNmBY0+3QIkhGMCevE/BtaIIAOjJWwCbPbhBFzBHQR+gqkDEybdA5pSTATDZRkExvH3A4ScxAI0Ko4FhukgAtZlywMrmGoFKxzTAGgXfwV97soC+bulAuUObgTwUfYBr14MA0ACOwI8gn4CjF+YAucszgFFCAsCHUeeAeyAYgJCryYAfb0aAnaQ9wADT7P/iu2ZAvh4C/7n+cQBSTKJAJn2s/6L/iUByTN4/rLsMP9Fvsf+vmi+/vJCU/636sz/hG94/UrJZ/4p9ej+1Qee/JAJoP6/PAj+T6dF/Igjd/33jaP/dHUJ+ySfa/1tknL+k8lA+5tPAP5LcDv995KE/D+maP0QyID81xaI/UoMo/2Bdpb7xr6M/aL0f/0HcZr7xHFE/vv8evxVnuX8pid9/pS+ovsNmof+/vJR/FrKZPyyr3//ZN/4+tvQ4v2ToWr/f5Z/+voDEf8qoNf+Sunt+v/RSP/rARL+o/68+9nkOQDJCUv92RpL/SjBqQCXakn9nDMF/hRszf+LD0j+Vr4Z/mpyUAArcCf+aXR/ANgEoP43ezf/NsOwASkXDP6bAez/0epNAQM1Pv8cDrYAiUB0AbTJb/64dlUCHckCAE0DZACit30CWLIo//DWbgIS1wMB6m8EAN8+TgNT+V3/IQNJAbnQhgNp9Tj/ML8bAsXu2wMuGgb/C1jKAoogHgI3SwsBpKoAA6RE5ABOrekC6De5AW9ymAJvw2oB0y3sAnaEHgE/mIQCPYyPAsFaCQLKG3sC25WXAQQ2swMApJ//7pHyA1/ZcwHo6IwAr6ZTBFiUTADwqNMBScOeAgKdOwGtFG0B9lZqAY/EKwGRGX0Bnl/mAQr37f/F6oACw3ZzALPFWwGpyUMB3+FX/2pvyQKI+wUApza3/4NVIQIMDer/7xbfALlkGQHZRVb/e64NAv4/vv5I/sABLCb7//ppbv/ww6MBCB3k/1rSY/9OKf0ALDGM/p++5QBeC3sAeUfU/Utn0QGqzzT+QQsGASTDav69CT0AR5Vu/z2dK/8Xi7D+rHFHANIjYf8EF77+98uSAC35ev2kDngAX0UE/m6FGACU6nz/vUVI/cKq3gD+uIP+Y08O/bobtgGfxU/8MRADAOUOnv/2NV77FwNBAlRtFfwbj63+MxS7AAgjaPuko6IA70i+/WdD+vwSX0oAXAYQ/IXyw/6t7Hz/aF4h/BE51P+m+Gz+Eroh/NE5uQCJAE38fLS3/JTeZwJCT9P5EE0tASBfFv9c0XL7et5CAuy/GPsDOegAHAss/tSFZ/z9YGYCFBV2/shidf3+gh4De4fn+/reFADL5+4B9Y6B+gPSgARQ/HX/kgfi/IY7cgPCxu3/mETo/LE59ARUaST8SUWi/4Y/OwR/Gq37ZZSzBLpdgf7F+joAXA+eAt3GdvyDIj4DaHDKAKowjf01jokEZZdj/naRCgE40HAC0Xjh/7kFlQBmiPkBY7Rf/zKhBgEJoV0DQW9S/JXJTgcqNk/+gyJsAF7b9QMULTv/oPtrAugh0QF92rUASygCAC/oVQVT/Rz+fnGHAldoeAP2QJv9maLMBA5us//I+ZkA9fwDBIAS7v7XXvQBA+EDA67jwv6rw6gCj7CYAVBK+f2WnlwEPUsP/asjzQIffesAqemz/Y8gygQnl5b9xyed/zJgegOnEH/8A0roAPpIEQJS7677sQ19BAtJSP1F6nX+GhOgA2SZX/ums+MA3IsGAhwmgPsxbOgBzL6+ADiWfftIbOEBAsm5/xwWSPwK+JkD39YH/EgmCAB5NnECdIWN+ov9+gL8N4j/ii70/C7ZvgCEG3P/VDI9/YvwpwHnZW3931hWALLo4ABJGsX7EIX/Av9ZHf6B2WX/hB8nAZsDwfwa3XAC6gVW/crqLAD/skQAzicc/5HWu/5d66YAnyIEAb6zdf2XNygDVju5/HsCQALCl33+8wTY/wdJLgGbYwb+u7V5AqhBC/8NwQoCsaYf/2vyDAAKduEAl0J7/22pJABp6v0AUaZqABR5nABswwMBsE2Z/0Z20QKLAjb/76a7AQ08d/9xABYB6EeCAK76bAGBVToABtYPAQcrXwGiqVz+cmMvBIAv3/u2qaQF6sx2/cRDFwH4Yy8DyMaT/QoJ/AIhD1H/dz+O/15KTQCGGpUBH5zU/rITOQHuYHD/WFA0AUBSv/8jCGf/knh8/4nvYwEOlhn+gFjrAPVoMP9LnQkAWU0g/4NI0f+19CgAQfv0/cnaWAIt7oz7JcV/AqTRVf01OBQBbGDY/soMif1K02EC1VEf/DTiEgF9w+H+m4Ci/rPqc//Rd6f/MXht/0wW4P3w8q0AM+kM/18r4P+Fpsn+qrW1/23Upf6My8b+HdPCALA6gv1NPDMB1Ush/vyRhAB1pUMArypn/5RnLv/OG5QA6F4e/l1qpAANomQAYFcA/qzRugL/a8L8ZjveAb76Wv/wrer/uOJKAD+5jP+om+AA5UoS/9cG8f+Z/8H/y0DVAUOn4P3d28oB27gWANNSCP6/JNADvEao/dle5AEiIyUAioh1/qkmaAKP7rv+sfI5AW9z5v9Cx/n/vA2+AO/PngAJHM//iIqlAW8tYQDrWDoAQjqFAdBPnP+ykOMBnK01AAn59P/vd6wBaOGB/z+3VwHZE9UALd71/54cbAEk0j8Amd4YADZjSAHV9U8AEhnw/8WCpAHg3yz/7C8DAZKZuQBCfm//fM38AGhV9/9LXc8AYv56AHpQvv/fp5cBo2Tb/4EwCABgRqQAdXLo/7jtIQFMBUD/MvcjAJ26WAB6Dk8AytI3ACWnFwG6mgEAGiVGAAr6pwDaluX+Ws6zAQCZHv+uMcwAFcnD/7/PXv8pKu8AS1hv/qxokgF26sP+sF0yAAEAGgDDu7z/OWlAACRN5P7y5vUAagvy/QQPrgAdpmL+Us8TAIgSIv8Kmdn+OwwCADVSJf6WUywBVsXX/cXM9gAIDXz+i62t/769dv51xiQAczwK/22Uhv7ZNe4A311G/Rn8nwAb38/9SHq8AMoYGP8xCXb/B+M4/3wZov+X55n/ArEw/geDVwCAOPT9imOG//TFxP88FfP+ITC8/3gTCgC09DP+Q22L/wLaRv/utxsAgLNm/8boa/+DNRv/8oqq/7+ZOf8HyD3/MZHk/0LWRP5ULtsAxcIv/mgURABO8tv/C1Fo/3w7+P/+n5z/MLcBAM/Vsf/sh+r/ryc9AOVFpwBSKWv/S2cfAcNThf+JglYAF/uuAI4oxf8mVG8A5aAlAP5CfQCPne4A2EqSAA/fAgEDmz0ATQVCAEp8rQAD6yYAR5xqAJJDuwDfujUAMTaM/2klqwCNXBAAlEjvAI+CCACOwAUBIhJNAKEGIADQN7IAYSCzACr7mgDmD3MASd5KALdKyf9A2tMBdvm1/9LeIAGT+L8A1WXzAESPmwAwvrMAsfppAQGzLABavRcBaJKaAMzH5QARdsIANPhJAb2qAQC9+3sA1CdAAZytHwBvj1MBgND9/+V3+gD/ns4AFreA/8GewgCF3EH/JFNzADEkIQDwuYb/w3Hh/5kytABUqjn/BSQSAFHWBAB8urr+qLrLAM84uv7VwvT/0m1U/+8XwP7LxCIAmBNI/0dL0P7C4y4AjIAy/yS+Ff8xHPv/6IUz/92K8f8kjjz/KUhm/10enP+nlmH/9HPS//36e/9BNKb+XxsuACMckP9/rH7/kXj5/4o9iv9Cwsn/hxWJ/1L0Sf8z12n/YmbN/+PNPv/oa8r/h5El//r1zP8MtT3/iPB6/3ZMI//cfPH/qfP1/yzfx/4q59j/umdn/8jdAwBo1LT/BkTo/6Pt8f6L5wwAikXA/93Ng/9fZPf/bZIcAEYPQgAu7iIAFtovAB04LQCWpoYAb5ZS/4GfVgC5Nk0AmPuSANrmggC2kwoA90E4AHLEAwHIC1n/2nt0AIJUwQAi7sv/zY8QAbgD4f9Mk1AAMQfCAJ6gpwBMYeL/Dop3ADmhBQCWvcIAZkAsAHzN9v8ReLYAovIxAIrG8v/xsHoAn8lhAIj6XQCNIMUA1hfQ/ybyMwFkkuL/HpueAOKQnAAfiCUARDAUAXauXgDdFZUAxusOAeypyADl6E4Aq+M4ATnFowAf+BMBokzYAKeuyAB/3EABg2lDAC+TJQFVs6oAdwVUAB0oCAGB1EEAlr8oAbwhggBRaocAprQ3ASWzx/8TwrgAACqQAEFr3v8PnmcADadHAENrRgC0Q3YADSwqAFNbYQB87iAAqKV6//EkQAAVL7n/QXhp/29qbQCTs2H/WXOj/5EF2v+L6Iv/yP/F//UzI/+EkVv/gLxf/1XdLP87Sir/a2p9//XpIf/LdmD/Sfp4/8gGHf8nlIb/1Wcm/3WQ8v76C07/cXDr/pR6rv80AeD+fJ1G/8EvoP/pIZz+3urB/4tKZv+F4Df/6fIz/wBFpf8CzWD/BRF5/6pFaf+n+3z/Z02B/5VPTf9lIbL/RnwX/z2C1/+SjuH//n9m/yN83f9YiN3/DK6M//E5vf+eo7r/fN6k/0GK5/87van/BpX//9+Vzf9UL+7/m0UwABXpv/9YTxwA7XAAAJcT6/9COgQAK9sJAAR8DgBR3IkAFDX+//GeSgBiolkA7NnH/4HyqwBCZsv/75VQAJzbigDA/tz/lAWvAJ+L4f9B6WIAxuWOACrnGQBbTaoAPW1MACa8MABefYAAOrBUAIdDFgDSL80An4LA//itlgAwo4YA/lTs/4juIQHTA9P/sMNmAPUSvwC/VP3/vbWcAJsRSgCX2gYAItJ+AGyXGACv11cA3gl1AOBfCABb4EkAHY+AAAqp6P9KJXcAQBsEANpuAAAJ+4wAFLqz/5JOqgDWz/D/gXj+/zobYgDFnBUAbWREAPb5LADBn0IAYgH9//e+TwD1rMv/qa8fAHLjIgAqctX/AMNcAMwvy/+O2RkA4bP//3ZFBQA1/V4ACrem/0IjNgAlFcX/DZvW/wvP9/+mEsz/prcgAPH1fP8kkw8ApXG+/zwMuf9WYrL/ziO2/1V4bP8wZLv/C9i9/yw5bv+BmeD/hbNT/9Fiyv+i9JT/oxaT/+NPof/7NJj/kwVc/8r75f/U8WH/94uR/9NA0v+KcVH/oCTJ//Ahh/9QpLP/+559/32Exf9Of3P/76HL/384h/9aVND/DzTR/7SNi/9scuz/6NS//90Yjf/ktyEAwIy5/62doP+I3FUA8Z9k/87wXACwt5T/x6Dz/+bcCADSzZj/0fEWAAhRzP8yluP/Q7wsAESV5//ynrr/s/BjAEbFfP+nKjQAvpcGAJA9rv/sYXcABniy/9GzOAD25QwAt2Xi/z52TACKJPX/n7shAIhJLAArORsATSkfAO7zOgDKzNb/FHJOAL9xEQC/ShAAJX5hAFVi7P8AOTkAb5VqADz64P9Pr5IAkaUsAF+ABwBuVogA5ykgAHKuPwDwZFQAMJtVAIkYLAD7IYoAJ3j5/yogpACh/xMAMelBAFz1fACo9QkAfxd7ADAtXQDhaQEAjKiYAL4s///ZDykANeWjAARhvf+x88EAlvACAE/UVQAFnkAAZ5BIAA1nIwDSb3kA6eoZAMGFMgDlTagApPqm/3JkvwA2duj/0lRQAK2AXACLGNr/XFt1AEyhAgA0Rj8A75ElAA9WAABATgAAY8gmAPfkz//zLAoAoUvg/421rf97jQ8Aa0Ki/zJsxv99Rcr/3Kan/+N6uP8B7q//XUd4/7e81f+5Ckr/3Le1/8XXiv9ZuVX/tg63/6mPcf+amYX/rnis/2Y0l/8RF5b/PF3P/xPWfv9VewQAPc6W/xp59P/l0b3/AFbd/yqx5f8+WMP/5/gHABjs5v8qk+//iKPj/xXLKAC6q7j/sf81AGIa3f/K6gEAprQdAD7G8f+nfx4A7hTf/yTrPABIMgwAYKsZAHhLLgB4/xAAKs0OAIq5IQD6JP//f+LM//3gZABGSqv/7k03AL9aNABFJKz/UFxrANk/0f9iBRYA4VAmALZ0+f/lyyAA8+IPAMAl+/8XUz8AWd4GAO89AAB4oWgAe6gAAG9CPwDeI2cA4LfU//OTlwD+BiAAqLszAGUglQA/G/n/Jch9AIU0TgCT1yMAeT19AJMRYQAtrSoAnOCQAJrlKQAeuWEA5olcAG5xFgBydYcAKL48AHFtGgAEe4AAbiDq/1CKSwA6eUAAXl3t//ngaQC/8vv//L8kAL/mMABqLfn/EKv+/6YGIgClHbD/jXVSAPDmvv+jc+H/yYovAOCvmP+C2g0As4vo/9Ldvv/mrO3/8zzr/6VJrf8omggA+ua1/0fR4P86bdX/vPvb/wvzwP/Q1Nb/k1Gn/wgQ6v/6xNn/Mj6b/8C/KgCHRnv/Bl7t/8Z/zv84i7H/Wp3l/1aYtP8jNNX/Yazb/4Z8oP9/oN3/lwvC/3j2ef/4IxMA6UOd/wazwP+i7uz/FE2P/1gNAgD0tLT/YtLH/yhO8/+hZbL/jiTj/1BQ4f/nrc7/BqH1/yzIxf9ByOL/zRL2/1Nosv82Pfv/yi7W/+1wvv+ZNhcA5DLi/9Isyv8jUCoAec2n/2vZLgCEt9f/+Azj/7E+JwB5TdP/fuoxAJlx0P+OpykAVPbi/5+PAgDtZhkAynD9/6HH9v/t5j4A4Rnk/1qyJgAeoi8ALHbW/3wWSgDVSNv/dpkMACHqLgAg6AMAgu8GAGn1PABnON//Vv9LAAMeFgBjBBYA8vVDAGH+9v/yNTEAxt4nAJ7bDwAIgi0A9z0UAK90DQCQODgAJXvZ/zD8WQDW4eL/JnQgAIq7QQCHKOf/umcZAO2QDgASZ/f/X0kHAH6sJgCvo9L/U94dAGlG6P/15gsAfYwTAHj69//mOvb/XQQbAImmBwBNrSMATT4fAGuvBgBBCk4Aw43l/30fXAD5DhgA5tYsACViPwAldyUAjS0rAB0mTwAgfCIABOgOANubUwDx9vn/RHo/AKy2BQAFZhkAx+4EACDlJgAcPez/TpcaAF1O5/86hBYApY7+/zvozP+AYxoAA+au/6C+8v8xq8H/MBjp/zAIvP+zc+//hUrA/6Sky/+RH+//TdGy/8ts2P+Jv8//ypHD/+Fd6P+A37n/VBzW/1e54f/NyMv/BgzK/1KJ3P8NaOb/dcSn/wQFJAAaoY7/VYD8/0mb3f/wCKz/XVEUAJrPpv8au/f/PJLo/32/v//nCPP/la3V/72x4f/p7vv/YjjT/0+m2/+P7/v/JIHG/yE72/+kpgIA0qy//5pHIwB9Nsn/TDMEAH4iAgB4Kfv/B7wjAKKL/f8dkRoAdkoTABMgJQDDnwAASdo+AB5tFwCqLykAvrJAAKYBFQC1yzkAo9AZAI1qFwBizzcAxi8VADVnNgDj8QwA8iZIANOkIgBtbjwA6E4dAFmpNwDohzgAuBMaAJOSWQB0Ffv/d81nABKRAgCipUwA1jopABnUJACTCVAA5bUEAFatVADLlh4AC1I7AFIjMABOZyYAQg4rAChwHQCCWkIA9ekTAMZ3IQC7gS8AO1AbAGvQFwDiZigAwDkpAE1ABgA7T0AAtv7//3WiKACh0CkAmVwXADrqKABoXBMAdOgcAP2uGgBwqyYAc+AAAKbSMwBMJgQAJaAiAPXyGwDUnxcAjgMcAG3Y9//8lx8AzZAAAAGCGAAJUvj/jM4hAEXa7P+tJxIA0NEYAC6e6f/0AzcA8QnX/zV3IQBRfuj/0lAFAAnCAADqCd//jTMKAL5s1v8L6REAfa3b/xic/v+yhs3/f9nt/7as1v/hWtL/Uq8OAKm7y//wAun/YJn4//N+2v8lm8H/PoDi/0vL3f9ekqj/z6bu/yqJzf+Q6a7/rBABAKfVnf/Zpu//VYer/3rGu/+0ps7/8w2t/1xZ3f+zAcT/f6vK/4+1wv8luND/ClKy/1yr6v8KssP/obPU/4AM6f9RwdP/04fO//3r3P8oQJj/+Xfx/0gLxf8tNsf/dkHy//enkf+XLr//Ztua/wcK1/8Evsn/GE/V/2fb8P8ecfH/OGHn/02w+v/g8er/V2jq/31u/P/G1+L/MSwOAB3VBQCxlu//HycsAEc6//+M7RgAca4QACkJDAD58iYAc4MgAFcPLgBUOiYAt0pFAB4dHwDUcmsAlHIZADdcUQBs7EsAv68lAI2fZgDaaD8AAfpQAIqJWwAMqD4ABys7ALGBTADggxwAjPtBAHRPAwBSYxYAapgbAL3k/P/Kku//MBDr/4c04/9Y5PT/mYva//Iwx/99C9n/pdy5/1w70f+Kaar/JUDg/6Em0v+e/L3/yNep/3KEs/9CfqT/FQG1/7zz1P+F1d3/tKr6//5L+P/I0wQAnP/l/yVgCgA15d3/oYIKAPZP/v+eRwkAFokrAFUS6//oeiIA714kAAc9GQAp9y8AqO4lAKJUNgBdbT4AtckkAHO1SwDvaE0AeedCABKSPwAxvkEA+6I9AJeEPwBBdFwAoLtdAOtRZQBKa2QAv6BbAK8OSwCesUcABMotALm5RwBXtEsAHVImANcRZAAgyxUAQWs+ALCcLwBaGQEA20c/AKf00//DpB8A+TMNABwH9f/AHw0A6ev3/zKY1P9hcsn/ZYjN/10Lwf+bFA4A7mnA/2yy2P8Woc7/SVqr/6qttv9xRXX/hC1e/0rMif9b1oH/9HPC/4QX5P+yaHv/ueWm/0RMbP9076D/GK3S/+JNl/+NmGn/9neP/4eGgf/3hKL/YMKt/+o0bv8veOP/oTab/3ody/+P/PH/1S3N/9G2BwDjCf3/sx/7/++xNACLSRsA+moDAJytNAAPEysAjk4wAJTxVgAQq0oAyfxbAGCQgwCUXmoAOgSQALSzWwDaImcAF7p8ANCNUwDkTX0AbStEAJ82awAWzmMAQvJAAB+rYQAcM2gABdtkAB1JMQBdsmMAGA1xAPEs0QDeU3sALziHAKi1gQD98yUAzlMoABVypf90fjMAp7DJ//bGqf/1T97/E6eb/27Dmf+er5//4yxs/2mTT/9YrlT/gv0r/xsYe/9Ks2r/vYmC/6PDmv/bzHD/Ec2t/9+fj/8d0Yv/oDfD/6WejP9xgdz/J/3T/3CQwf94lOj/wXax/+W5o/86yuj/KQ2W//244/8UX/n/ZpzB/8BFAQDaouj/xv37/9XPCwBSqAIAtWze/yUnKwCyztr/5FMkABVkHwCPRSUArT5nAEQOMwA3XWQAVYAoAMamEQAcxTEA0vkjAPRpKgA9Iy4AaTUhAJr8NABf9SkAtywDAIL4IwB0ADEAXPrB//T8MgACX+T/WDERAOj6KwA51fj/G5EbAGMM+P/yVhUAgSkRAD+kMgCFxg8AM8dUAA96AwDuNycAUFI1AM7KRwBH3BoAvIQGAEP6EwDAXRIAvSMZAOwdKgCx6GcA7p0YAP9VXgAycyoAB4wlAPFwPgBriUQAsAQoAAdiIgCA4zIAkTwmAGgmOQBFTkMA3LYrAMP9IADUFB8AwEgWABz8EwAYwDgAy+0oAC0uIwC4VisA0iUAAIOPKQCYv/X/mgwUAKQ5AQDASfj/nxgKAK3L//+xRA0A/mARAJsTBgDbCQcAHTv3/wu44f+GC/j/ynbT/5B31P9hasn/+unP/5SL3P+3/cD/OyS+/+pd6/9OVrn/FZq8/0Ir0P/9+rT/1H/P/wrZzv+uedL/9JbZ/3gg6v9UqN3/1WTj/0kW7P/6H9X/MfPb/y+J5P+qmtf/ujzs/+bc9P+b08j/ZHri/zGi5v9wDvH/zucKANja8f+KAej/o83g/0JV+v/sS+r/Gnzq/yAt2P+JJP//DDzX/43V1f9JBPv/xb72/w0YJAAe8/z/m2YUAPqpPABdlzkAFQ4pABNxSwDOFDYApCFDAD4xMAAZVS8At/5TANSzLwDI4D0AdrkqAGQcHADAfUcA5F0kAIBUNAB9lzsAQDYlAA1yRQAWQzYAVUwrANOqKACQ5R4AFCwJALGoEwBzHAsAMb00AJXWGAAJzRkA/PUdALniDwB/DhQAxWrq/9Ir///DAuH/SXAFAFTj9v9ayun/SekBAE3R6f+uPfT/97fl/3EL3f/pWMn/Co4GAMskz//VLMH/4qbs/90H2f+XivX/c+PB/1NL3f+CB+T/3obV/4aIuf9qD8//2JvE/0sivf+yp8H/uYmh/7Wb3/9gHK7/dOuv//AI0P8Dman/M7fG/x7Xtf+xN77/zcbG/52sq/9+vMT/xSCu/x4H0//GE8j/RDK9/0ki0v9QgNz//lDJ/wM0z/+2KuD/wu3B/2rm3f8izN7//NTc//1e/P+Dpvn/YqTl//0A9f+xXwcAYuz//8YN4/+FpgEAl0rY/52kJgDs8+3/b8Dz/+v8QQArqOz/Fqo1AItqNQAkfRUAuxs1AN7lPwDsCCMAKkg+ACzxQACzaFkAl1ByAFFjlAAh3ZMA+6KFAOpOagD5ro0AmNSLANdkmQDqN7kAKemrALDC9ABA5bUAl/zQAHZO3ACw8a4AQrSgADZerwBEq60AYUm0AD1N2ABk3roAftLrAAWWuwDr9HwA/3WNAGk+fACviXcA22VuAHpecwBM7UIAUbFGAH+UHABfGQ4Asw8gAFD82//nV+7/MFvv/4m1x/+yIN3/Q7jU/zSfsP9GCKv/aKOU/1T8n/+p72r/wXV//yJ1eP/FEVb/IkVg/2lIaf98wFv/L8Fh/8pqJf/tmiz/fSRU/9RlGv9WUzP/pwwM/7MT7f6OE9L+XS4G/4+Puf6mP/H+/aDr/vmFrv6UFKv+DZSn/lgOsf4ZCYH+6/ms/sodTP7OtJz+Wnln/vAUY/7Zka/+r1iL/ro+p/7pion+X1+V/uscof7fZa3+8ieZ/vIUq/4UnNr+kXYK/5WWVv/mEEb/M0U2/7bUT/9sS6f/aM6Q/2CBeP8H/dH/azm3/zzu/v84T2EAw0aOAEaNtQChj94AZYi1AKEZ5QAWpS4BB6FJAXWfgwGKA7QBwUC6AbNUEgKdsTsC9PVRAuzTegLlXi4C5vZ3AiSsbQJdCbkCXZ30AgtaDgNJle0CqA7KAqf3sAJ7roMC0FOqAm2LpAKyKssClbinAqpCiwJRU3cCEPViAtGMMQJtnQACo//bAVq5wQEV4pQBJr6SARHmmwHRXVgBAIIVAdUe8wABi8YAtXymAIIweQB9+EkA+401AMhtFQBhGw8A7yPV/0ZCvP+nYcv/waN1/8NL4/4g6OP+wloK/67V3f6+Fq/+GwKs/qK9kP5/c13+TBs4/kuKyv163c/9mYfz/QuAlf20IML9tKHo/WPSt/2FwGz9Hhuc/cwnYf17u9v8YqpP/cSeQ/0iJTb9P44G/QGsEP27zyv9+1Xt/HrU7vxGDSL9v0MI/QReDf3OB1f9VhPl/OFoZP0NB4f93n6U/aFxef2cn3f9GiQn/nhqzv3qi8H9glsz/s/Qtf4qLFn+EbPS/mtEy/5gXQL/XNuc/86UU/95vKD/wFKC/3o73P92bcv/SHYdAKR6IwADJzkAIyGQAA13awDLb8YA1PfvAPWBUwGCRToBTHpMATqdQAHX85cBHd3RAZaohwFVYe0BDUUEAq+newJ9wMcC6/AxA8gMZQOnHVIDiXJUA3LCQANG4IMDYY3DA9LBKwSjuFYEkeiXBJBW8wQKNSIFcaYfBZWNtQS/Ak4E2MYIBLnzwANYuuIDQ7D+A767FAQxZRIEWKi7A+LvUAPcuwYDiBqYAsdL8QEqWZ4BVQFIAeMqVgFsv4MBpptbAfNoFgEjnsYAWr0BAHNVXP8dmC3/hEse/wgWKv+julD/bdhI/6wwrP7blHb+JJ8J/jbglv0S83f9mrFH/QVQMP1KWFP9YYKn/RS5d/0DuPL8z7cl/XyY7vzUaJX8YN8t/Y9JJ/2HM7/8C+Rq/Td9I/0PKCX9WXWZ/qV0S/0z/jf934zT/bY3vfyo3ab9UnFc/lziO/0aJdH90yGj/e0Emv12LFj+U37+/H4J+v2tXVv+B9uF/S3pf/0iF2r+qjIa/gI5jP3FgKP933X2/ZG8l/5R9vj9RXZ//rHiGP6GEoD+tEGk/oKSa/7NWxD/ZXEK/07HyP6vT7n/nt9M/7B9gf7qn4wAXvEl/xtLL/9oEkMA9QV0/2In6v/lZWkAGIc3AOFVRgBZNswAL4klAE8VEgH8UiIBDSXKAEmDOAHjuM8AEw7yAMIlKQG843wB/btuARoyWAGVAC4BsWaGATMRcAFvxEEBkVoCARekJwEOwhEBD4nVAGWJYgEwh+wAkdunAbp6SwEr6yEBxUWHAQzCfwFmapsB8w1cAap8gQGn/EIBUh4gAgl+WAEP1NkAhWDpAVGeFwHZPhsBTiS7AYlKKQEmWA4CSjmgAjU5IQHUa8EBdkukAcstFQFz4OkBL6ViAWHRMQI/rkECVcZQAdE1GAKmLSkCQfzfAU/eiwKJfRcC+Q3qAd2m+AJLnDoCmsc/AiUjxQLYMCsCpyxuAoF0wwHDRWgBw3UTAoNvmQFEAK4BlO6mAdDsgQGMzsQBzmfnAFPNbgBkusEAhU5vAGhXIwGTDKkACbq3/0wa5gChdvD/Snga/4lQSwBVGeL/kkwF/1qjNgAphQ//vPF8/oxgTQDC8JL/ayqo/aO7bf8ZyEv/wOG8/UN51v/3+dH9Im5i/SEeHf/gsr78ERia/UPDAf+5Kc/8Feec/cJBSv7SZnr8BMYe/l+cXv7GBYn8BpRt/g6Kn/wuN/P8TEWC/tUYFP0HTFP8HPDu/FHFtf0VYwD9us0P/Q8W6v1NDtP9u2XA/PRH7v2prk78twFn/UyDIf6BDAz8bZCG/bVmqP3FW6b8p6+B/lLEoP3TDmL9Dmt//6/JJf2BN3z9P19f/xvvrf28krr+n/49/6+4Av3vcxX/bxiM/24eof4PxhIAfbDZ/sLZCf9tRK4AlgDb/on0TwC0EnIAGnkUANUzNwE1Lm7+dGWnABqOigIY5Y/+Pr2kAEVApQF+THUAPI+LAiNpxACsaoMBoVg7Axj0TgAG97IA9Ny0AqXH1f/Etr8CVycoA6cAAf8uBFkDFAqfAtGMqADStowDWtJmAb2J9QGQSMgDVEgoAd4QOgKbYL8CgpIRAlaWFQJUIwkC1NIPAtnNGANqxxYCBUdCAWbPkAO+lMwAh2cCAuNhOwIIv+QBVTcsAm83JgGDCB4C++kWAZwfXwGim90B/iMGApCVRQAD1WwCV7yOAVr9iQAK4eMB/lo/AP9/bgEerIMBRF4GAEUwev/v+xMBCux4AMiR4ACmkpEA1iPP/9XVtQDtK3z/0Uq+AGjRtf7ihZoAsrtbAXoa6f37jmYAfRU+/7zYHQA3fYYAr7Z2/jPFZQA4W5YAYW0r/67W+/749BkAGkZc/0xgoQBy+vP+Iw9b/XXAegJWSSv+xL7M/aa5RwJMMRr9fwlG/63jdABVzK78VwyYAJL/1P9uqoD99ozb/3dXhv/N+0/+Vr+eAMMRRv6dIz/9cRjjATtvtP5h4p39a+FRAeLOOv+BJIf91G/Z/3xLN//pjkL+vxXE/56Ws/8rXr39OVT7/3TX4wC+hF/+XkETAO+p8f664b3+zYUaAUF9Sf7yj9b+T2Xx/4KkT/8QkRz/Z759AIdB7P4epy///f9hAfqWTv3+NngAB30XAKja4/8greD+VmPCABRrGv/L6wr+GQy5A/bGkfxU31D/MalxAqt3f/5c6/f+7FCCAcX4EP9opkH/eLdfASaeLv6GXZIAQFnu/wSAa/4OAdoBfqAY/iMO3P+mKSEDSF83/amGnwA4dx4CcggD/fEwDgF5TT4A0UtS/VBoUQKb9B0AY9c5/j/iBQGbIDoAZW9E/ZS4rgFYAzYAm1Nd/f+EUgOO3dD+794M/3r52wKxl7T9vRKKAE0CUf+jFmr+HwTHAYCFFwD4lvQAZIBg/pP6zwLR0fH+PV/I/4WI4AEAbAX+25IXAnHf5v55QxoBiJWh/3PUSQBbZ1EC8KPZ/bYepP8KIdQCi7SB/qrqyP8wyX4B2rTE/oVvsQFuRDAARs5e/4l/d/8Q8OgBDdoP/1qCD/8LOnUDwuJx/AFBHgLh03oB7E8w/Uc26AH+raT/0SogAFuEiv7pBMoAa6Jx/6lfv/7zE5wB62UnANTzaP8EkL0AuoQM/306r/+3Y2ABHV9+/hxc9AAGAqUA7yH0/g05ewA+wzAB0/zk/dJqqQBM0kEBkcnr/d74hABlClYBlUJj/1qh2/+7pUgBDbe+/xPM1f92jggBb0sAAJ6/V/+pcyYBl3bj/ziK5P4xDiADNMY6/2Rjbf50NL0DC5zG/IzqrACyC94D11X2+/JlSQI3TGwDuDVw/AUkcQJ8rx8C1rHw/fOXagC4fN0BJt+z/qwN5f+f3uoDlNnH/YHPdwIW33MAcRS6/ulQ6wJoRmD/ba1eAa8jKwH1YNL/eXu8AKZtsAFLQa//ja/G/wQLiwHLnqb+7a/PAEz7nQHA8Cv+Mv50At59FgFUNKL/OlYmAFAp+QBzsHMAVMD7/ra6BgB9v58BIMaCAAfyPP84VckBsuRP//K8jP8JPv//fXLrANJ/TP9mqQj+zAiyAaKNJwBWEVP+zRrY/2uiSQELmtf+2D9g/vp35QA90QT+Tz0XAQcY+//qYzL++Ia0/19ZWgAn1Pr+tu7t/HyfWwHN/k3/iV9B/p2IEf/Hh68BuVmB/oxnYP+0e08Ayti6/VAFoP5fLKIAtKIlANsMH/zyEgoCidM+//MXFP02SGUBiEqU/fzuhf17TtABJkuv/XDpX/40fkwCMQqh/WxQVf+FGRQBHZEC/QlZKf/kZd0BE7GQ+xvRKP+pgL4CpnYr+0jSbQGPQCYAJL5c/eM5pwGlBqj9EITv/pynbwCLEpD/DvqO/vOFQgDToIkAKMc9/v9tgP/I9YEA94+4/mG1jP8RY2EB73u//kaJ+v5RopUC6U2H/1XLbf5K6YYCnn8gAEWQSP4B8UgCLY8BAB7tx/7aTwEDO5zF/5r5Iv4mq8YCyodfADuEtf67OkkCaCJBADQacP9mePICxFmEAJjIEP0+nBYETBZr/mKruP9vR00DRu1J/s98JgGdGccBeYxYAEg9sgCeC7MBSXsWANnfGAEnLDIByGUmAJzmugDSvVkDTsuR/cHhpwGpscQB4veD//elqwD29E0AslRoAWhCBACZUQEBvvJZ/4L4WgPRHYH/mJTa/ppPNwPHN13/vgyr//XC7QLZ0Cn/PU0T/5UqZALRG0n/Dk+u/ra58QHbx+f/0uY3/wK3AAEvbW8Anyg5/8qAOgBIK0MBWNKU/ilicv9StvMAlLC7/v9nUv89btYAPuwk/3pUZQDkAcsAmV3H/ZBdsQHkfSIBqUU9/OJQLAIn/7MA+daz+/FNkwMU+DAAuCmE+uBnfAOH1/f/XJl7+wncKQLuHIwAc4qU+ycucgKDetv/CQnC/Cf0tAKxYaP+Lf9c/WGfVwIwv8/+J7hu/RK+sQLkU8H8Gy2GAEJGowF+icL8nhqWAmWf/P/ycOb9uPz1AJ8Biv9cV7r9ZIA2AX7Z4v+GOKD+DucgAfLmSf89gMH/7CJ/AIgw3/4clQj/b1JkAnpp7P7zOZcAu0kkARU4awD2/Bj/kMZ8/7uAfQHMYOX8t92QAevnYACRLHv/pcBRAZbzIwE9vDb+HLaSAamivgB5cAv+ukHZAM1xhP7Bv+gAdwFaAEOX+gCPuoYAH8c2/08xPAFbYJb/fFew/8l8fwCAT6gAFPwXAGjVewAkUK0AFxIoACjSfP951G3/hhbeAEbKpv6HG0IAzrkTASGIKQC9ekL/r5r4AEoKawD+NC4A3ZJG/1aYowHmeowAm3ij/Z68CALt1yMAd/oi/xhNnf9yvusBgYuy/mO6lf8KoEEAJ6S6/5vbNAB2yef+oNRnAXJ3Yv7Jafj/ifOVAPyRXv01T0wBjO6oATGK1vxkbLD//ADIAxbk4fvMkYj/059sA8DxfvzgITn/aHO6Ah9dXf6ARL39drIDApCBUP7dF9b/vs81AHJgLP8dqUAAE5wgAGDBc/8m64sAR6XZ/9XNP/6mGUwBpbHP/1txl/1QlZIB7YbEAWiqYf3ERSkAC4R+AgMI2/5AJiP9x/4gBBuaJf5m5Zn9T1IBBEmXk/5+zdH+/jrTAfNTlgCx0JH9zM6iAbqSQACKstf+1nH1Aeesif456GYAENz/AC0uNP92mbX/aAnoAD/PFQCXuZb+neuXAVMtsv7f9m//3iKWAlIIzv59HpP8XwERBWBbyf+pjub60PTHBe3EM/+H18v8Z+cpA42OBf8iiEz+vqj5AZzkEwARjXb/4nrTANT84f+okYgBMd6WAO9bkv2lIl8CZRxbAFBFYP/+gcIBH87NAP/Lm/9D2HUAEsNd/6Z4vf67ylsCJBU7/1G6VwD24HUB9xbQ/7VASwAVNcQAIz9Z/6srtgAgFz8A2L9k/+gfBgJwKeX+d0f+AHqaswFE+Nb+Ds3XAK6xPwA+LzQBzBVe/ixXigAeC2YCl9z4/TAywwB8jH4Ced9r/kwPYf5doe8Del+J/pu87/wJBXQD/gAxAQzqHfx9f/IAjgDeAnWg+vz2UwwA60OeAv2DJf4/0Mj+SB3uATTEWgA20P/75kB6AhvcvwI+mcH7uL77ANUj3wHYh2z+WMeX/iXxugHWDF/+VUxQ/4ZpBQL8cc/9rCbc/woCAQIaoyj+qaj6/gkkGQCVefz+CAMv/4y2PgByYwoAJNYZ/6PVmQD/EAj/3sD0/uVZJQBhbdT+fZDH/hGzVgDg3fYAC5jc/t3CQABjnS8BkI5f/SXIWP8jDjUAjODO/ZNDLf/XcSsCQAl2/i6kGADrmF0BuQlR/yfHW/5m7GP/39rBACN+zv2IYgsCCykL/1lbcwC24pYAXeWA/k3l9wAISQj/Q2+7AOpbcv6Bv24AXghgAPNz7f6TyQIByLwBANEpvf8veZn+t8OrAX565/6np0H/ozEUAoX2T/0k9qwB+2NZAAR2h/6LF0QAqkByAS+UVP9MhYL/KEVyARJNI//EslsA8Aa8/r1+uQLLCFb/kL73/oyG1AOCALn+Hm1o/sEFHARWA5v+qLQE/XnzJwSgk/P/tTvY/QtZdgJw5dgCFhic/vuLDv97bbAD0WFy/Rs9Nv9AMGICEELm/qKlvQLmd7H+uQGWAJwKMwN8xyv+sG1e/ZZbggQupPj9j77A/HROUwbJOG7+jVBU/bbhyAMpXR8AYamr/k+hSP+2J1IAi45vAXGFJv71PGP/ATHhBPGGH/4d1Ln8/1CEBPEbXP1LvK3948WAAjYHtwDTARP8cM00A6skigGTAq/8sdcFAfboPwKWU+j7NKyf/v/9IgT/VsP9pOGK/g70QgHrP7gBiul8+y/GyABc7YYBq0rv/YBee/+CAJYBAR17/xDzBP4qOosBT+paAF0qKP3vjlwAEKv0AfUTAADDVTD9TrGcAB0AywLWnkr9/aRG/0jlowGRkmQAFaTd/UuJzgIV8zD9IlxyAKQqSgKJgk/9VoXe/6zKpAL6GOf+KrRa/UwPkgR3Zzb/fFo9/3mKq/8ElzQBW/P1/97Mp/384vkB3tS8AMhp0P/DHMv+RPagAfCr+QDwF2L+0pg+Abn+B/8gqEIAdxQ7AbUQN/9vMUz//L3sAuheu/2zR9r9Q/BrBDJTlP1P7wIAaxpsANo/JQDQKhYCm+y9/olVD/+ezHQC/ngk/hnyDf0vxZsFmvwr/uvm1fvClwAHjJiO/un7tPsu0DwFpWU+/wZR0PycZMUAYQsuAZfXXv2o0TEDceKNAZ5MaPyrYgIDGhltACfTI/1o6CgBmS31/iAG+v4iKwwE2i5q/AYLHAFlvtYEGO1e+wk79f5RoMIF/rMG+mcvhv/FQwQFtEdL+pr00gGtYm0FuIcn/Lp4v/xhzZ4EcKYv/2AO5/v4riIBtcY3A8B28/z1MFQAK16gAVkVsP4zbsn/eFwFAXEQOP7VGUL/xvCIAZuh2fw9GQAC/Stb/yzJ8/4nQTcCGPC4/p9isf9V7UQAzgi6/9nh3P7X9PX/WT0vAZJGI/7GkSgBQnE3AZu86v5IIlEAet9QADm4hf8q3zYAwEQ6/cpBagGyPSgC4Feh/BJ0LgBWfGgC5RYEAGxPsfxApFgCYOLT/nOSOf6Vn04BmAcc/6VWxv9puNX/eLwRAcNcuP+ZV339KnSXAW4GOgEmTTz932nq/5MNsgEWIhAAdln7/pVsbv9j7uIBHB18AEZLK/3qUbkCfuu1/yw2jf/1lAIAUdZVAXdaK//5Yyr+2SIuBMFl1/xM/8kA6cdJArP4Vf1gQbABPkIaAbMiFP4DLyIBc/SCAQUH0/0Lox8BNNmBAERCk/45+2YC7KQb/wFze//HQL4CMNFDAH3lGf5IWNwBrq6J//qhnf7wyKMAfBkIANLsEwFqxIL+ia/9AV31AwCWtsn+1/FTAWZBUABqWKz9VDgFAZS7XwHkfI79V43MAcs41wCCfsT/scKK/0Q2yQC3BqAAlPdu/y3sBv+qIU4B54sXAczRGP7zh4UCKJeO/huxT/7K9x0DZ2YI/+tjMf40mR4CR3pZASh4zf3+Y2MAE2g/AQg0s/2NM2gAoHiVAFPq//5VK/QAu1Zd/6Ujyf/abEMAA+UP/x87uf9FfeX/gaUo/9kNb/8I940BThes/tQL2P+Lq7cAstD1/L638//F7xABZ2Uu/x/F0P9T3SICiiZi/9jqHgBYZrL/qVsu/m0BEP/127L/TmGoANHFTf93lcYAVYZxARbU2/6gvqb+iTy2ANEcW/7dbp//JyUZAJl45P5GimMC4MXv/2S5bv1e27MAt40/ARt4kf49XNf+23avAbbH7P/rO/X+HSxKAPPgoQAIsCT+f7Ps/1wvwQF9pEL+XOuYAO+jpgF+rjP/hj7A/6bjgwBypCn/O6KN//gJnwBIOpz/vXsoAKu0yQCkHeoAFdRaAG3Wmv5O8DwBL+9+AAWY0v5eDRcBbuxJAYA5Lf8YEloAz6szAapSD/9+FY8Ala/4AA/rqf+qorcA2LxFAWPzx/8tKAoAe/JdAF5PLQA2mwcAqbrW/yiWcADetpkAcIXj/4fyWgCTkf8AbYil/87X8//E4D8BecRy/6wQ5v9G0IIBpDgPALa3+P+56wEBH3d0ALj72/+09VgBxBQ6Af7iMQBg+D8BXilTAUNG8v8vipUA8LoiAeBsCQDd+WwAb+SGAQgA/ABPBqQAwBoXAdPbfQGIilAAI8jj/wez+QD2GNP/LRy3/1FQdwB0e0YAIjdZAPZ7iADF14cAH/peAN9d/P9tNiYAGrMQAO7Ryv8vQaj/3zYmALd18P85XEL/pks8AGpQwf/Qgl//FonZ/3pc6P/B4nr/cx6B/zEB+f87V1v/LTIo/zIODwB4hpz/E44Q/5nXIADkkoD/TA9B/ysDs/9LNMX/T6VY/xbLgP+bVLr/8Edp//qrs//tVD7/CDvy/+Xwbf9odRv/MZKv/+Ztaf9PFPD+F59X/8pCBgAAF7X+ZARu/w+8WABhHpX++s93/zC8JQBr9LL+hPsP/+ikif/lxov/l+S0/meXbP/gzb//xoH6/hnsnf9IxnH/uUsV/8tckv8+MZr/AZ/j/uQGmv8p+1T/5n1g/2T6ov+Od8b+WqNBAKpIo/9BT+3+Ts74/9ik8v9h4Bv/Oqe0/4ZnLgC2ZyD/b/INAGS3uf9GIMr/nVr2/+yhj//Kq4X/FPIdABL3pv/H6sL/ALxgAA42Qv/h4R4AgVoaAGNUef+5peb/aehTAD/spP/i++v/2qlwAIYjvf8uctD/Am5yANEWmP86X+v/qTxkACqylP/WXwQA7ckuAD6sHAAYwZL/FewMANNSNgBxF67/EQ36/1fpZgB6f7v/q1Xe/yvfrwCMhmb/72sYAL9klQCUOcP//y4NAE8rfAAMjiMAi7io/w0PrQATfuL/lXIUAOmEfQAuziwA/T89ALuGXAByxisA4pX8/84LbACBIQAAJiRKAD6fKACErVIAeb9TAGlWMwAx0B8ArFVRAO+5SgAAb///KYYyADDlOAB+Nz0Au51GADruKgDvr4cAYtwnAHD19P8/JHYAlir9/z9dHgA5+HEAztouABvgRABk0RYA0LNxAJLUHgC0t+f/JgjKAB1X5P+n0hEA8NeyAKlgAgBmMOD/0hHDAM8lMgDTvvb/g05qAIQCjgCZDQcADAlZAEo6pgDGKPD/bdRNAMliiwDSFAsAXztEALaBtQDBYU0AAukyABHKdwDuu3sANF4hADLgYgAswTEAS1ufAAIeDACLq2oAqNSbABL0KwDlpjMAV9FwAM5vRACt1xcAFpKZAILPEgAKFHQAaSVjAPNZLQCr1DwAnikmAA9BVAAwW0MA76kgAF9bYQBDBz8AjKdYANJlKwB0GV8AlMAtAA9uXAByr08A++gaADNhjABUUAQAGb8VAFoqYQBBmRgAnyXt/80nbwBO9eT/DScFAJ1fhQBxOrf//C8tAGdNOwBcW8f/acQXADYH6P9S/xIAZaYPAGHk+P8SqyQAPFPv/5q26v/Idg4AM3XK/zYvEQCqfff/up/a/wIKEQDJKAsAR8e4//Yx0P//kVIAOAHE/0N80v8W1CIAiBPr/ycnmf/qA0oAwMYJACNjef8DQmEAKrj9/5pkpf+T1er/zAH0/xQoCgDn6LX/B8Te/yU9OgCOPbT/BDnL/1+fBADgQs3/8BDH/4iP7P8gss//CWSQ/5B6GADyXNb/UPCM/80Vv//60gkACaOr/xDrj/9j0gwA+rjI/2PZqf+SHcP/tEbH//gEzf+f7a3/FuDA/ybR9v8TeIX/Kyrj/zkkBgBjcG//MuHR/0ceKwCyRjr/tojo/6RjGQDbMn3/u1bu/4rp2f9838P/T8jH/+hS4P9qXrL/XrDc/6kk1P8LUNX/ibWo/2tEHgApIMP/Wt6F/68UOwD2HI7/LWeo/16EFwCYhNv/AeLJ/xBKzv+c2AYAUKXm/8kyqf8+edz/oh7y/0Oz0P/sO9v/novt/wwyw/8yJwgAjCbg/+PD7P8oigYAR+Lr//JlMwBsjMv/x5MZAI0GGwCVmdv/zwkAABZG7P/MbmUAbMOy/5F2SwB8oEgARIqg/3E0MgAPWx4ANab5/7VLyf+6waIAZf3M/wBa0v91Go0AOJyg/9YbAAAr108AhZjm//KG7f+kUiMAQBo7AMYSIQC+NrH/w8pCAC6INgDD4aH/TA0yACPE+/+IMg0ACEE1AL5Y2P/JPvz/C3YBAN+g//+598L/wubv/2LhQADhsbX/onAIAFYSJQAUTcP/+RUHAPLGEgBFbr3/KVfw/yLPRgBZJqT/p5Dp/62eWwD/bdH/k7q5/3rFXAAVevP/O92w/54eGwDVQwQA0enc/z+kuf8dIDsAApS0/ySex/+cx1cALX6D/43OGABl6hEAiDSE/4OhCgBExwYAMWe0/2Xh6P/AhRkAIiLQ/3ShBwDJ9dL/U8j5/1tI2f+VftL/huhBAFxhqv9LrO//7+hGAE3n2v/K783/7bYnAHnCBABm5rT/mbgjAId1GACludH/sOMrAOAGMwC0dOT/t/UfAOE6KgAJzfX/aHwQAAhIHQCTpU4Atv7U/xIcLQDCS2QABSC7/8i8OgB2rFsAGk7T/5HKDgCkWVcAkun1/z/RGgDuMScAeQImAPMI9P9cQ+v/v/k5AE7w/P9UqwQAiC5EAFd+EACT6xYAoLJOAHkN+f9O8vv/9l4wAN3hJQCetfX/gxUpALf0NQB7e/f/uwUkAIl4+v+hXhAAk0ceAJsT8P/OLxkAuK8MAPKcGwATlP3/4DwdAGew9P+1UeX/QI49AFq52P8SG/T/LaFNANDm6v//9N3/rHhPABqf0v9Xtuv/qIk0AMzl3f/govv/PEX//61G7P/omN7/SUcZAIfA2P8i78X/98wXAMug4f/awtz/6UQRALRRxv/uEgkAwFXc//1uzf8UTSsAy9q//ybEEgB2gQEAV9j3//tm/f93YvT/FFf6/7Gw4P8sEQIA2V3//2Sd3P+/hPj/28gaAGzs7f8RP9//5S///9VB/v8LgdL/1oP5/1oTDAATXOb/XmkDAPya6f9mwREAn1Pb/9VX2v9O9QoATKHI/0rSCAB0Mv//ycTm//r84v+aku7/bsjI/z+19P+8uOD/Khj7/wrk8f/tJfb/LZ8RAP+jBgBpcPT/NL3p/9rD/P+DGwAAXo/v/81e9v9OIx4Aqtzh/xE1EAAnEuP/TIkCAOWM2f9P2wsA0DP5/0TEEgDU6O7/b8P0/1WFJADe8rT/Ax8qAH+P7v8FiN3/mXQKAB2TGwCpCNz/gSL5/4lNHAAu9M3/MjoNAAVNFwChnfP/ay8WAMCBCgDyPBwAAZr3/6ZPAgDaFxwA2LH5/5TRDgD72SgAWDf2/zv9NgBmZB0AuP7i/700MwBn6PL/HZ8FACBOKgBoOiUAKyINAPpNQAC9jAoA6LoFAEYrIgA1MSkAkFTw/+a5LgCC/0MAoGAJAOSORgC2wBcACIE8AL47HwB9DTEAJ7cpAA8EOQBgyikA5Js4ACDIGwDxo0QADwURAC/mDwCUqVwAS7/y/9zzSgAyICsA4zoKAGNhPgCV+RgA7AgRAMXmKwCQyiEAshUfAO/vFgBUOSMABbr+/5FUNAC3j+L/Y2UpADvWOACmsuP/Pr0qAIrjJgD3iQwA4lLV/wTkTgC+udP/E+jq/+SiNgAgsMz/6Er5/5fBKQBrftr/LFXi//ZsEQBRwN7/OJ7O//BS/P+EFP//UdzY/9q3+v/f+e//g9XQ/xjR7v+r/u//sSfp/wsmyv8Xu+7/Z6cSACM1pv+J2CwA/F3Q/z0t4P8EnwgAmUjX/xyo7v/pJdj/OeokAI7Pyf9lfub/XPcDAFL85P+UbN//ux7s/1sa3f+bOOL/14Xh/zMY1v+eb+n/Qtve/6k04/8nqvr/E3vL/8GS9f8A9fL/yc/b/xgw8P85OBgAHcPT/9s+//8j7SkAAIup/wc9HQDpKwgAKTz8/7XY1/+6RC8AzNcEAHWJ0P/vZycATlgCAD9a8v+ZDAcA54gTAKSO6P+xTisAr8sNACmGAgBvdxcAHVwNADM9CQBaOQUAAv0tAOhiBQAkHuv/XNNLAKZW8/8NbA8AHawLALOGLQCfkDEAJXzo/46+TwDlrRkAN20GAONwGwDyWCsAQxAMADAXKwCf0i8ApV4DAPllKgCL+jMAhhnY/5VIJQDUexcAHtLq/0ZRIwA/nxUACOYJALpkGgDfEikAlBsBAPgMLQAPdAwAHNIpACkMBgCwHSUAyh8yAGu46/9+SRcA4QT//w009P9F7Pz/q40rAASpFgCF0wIAE4YoALUw3/896ef/NZ3Y/zws1P/TKvn/fMjl/2gr9f+WMhIAz0LT/ypn4P8+EQEAU3qk/6+H6/9U+f7/Oubd/znv3/+eBTkAiUzq/++ozf8SCSYAoxPl/3Om4/8mhyQAoMsKAE/DFgA020AAEgYwANqAWQCRq44ABHWKAO5IxQDUFyQBN5EEATdaXgDcUNEAISH2AXiLJwBUCZ79o4QC/RyEb/3eLY39HVlZAI7QtgN8KpcDkZGuAULAmf/iX8T9DIDu+/jrzPt4icr94orU/58pzACE4McBqTfKATchRgAiLHX+fLz9/XMAC/6hR4z+ZVoGAGCSQgFp97AB3R4OAZj0aP8znUz+5Vgn/qVen/7xrRQAK1lDAhIp2gImahwCoD6NAIh+bP7QhMT9Wc4p/lziLgB6KR4B7JX3AcPkawLD3cgB1ZRa/4T98v3ccjr+z7E+/mnyWf+5k30Af/qwAnRikgKS/q0AmW0z/1bxqf4jV03+OGr//TB4gP/k/B0BkKwqAXww9f9p54P/QCRdAFWPFAA3uBYBd90KAi50LALtLRYBUA9cAIiDVP+vy8L+5OAEANNuNwCI15kAVIQ8APrqyACWX8b/k3Yv/1Uo+f6rZWb/BmuU/7BhJ/9m91H/wHke/+dQcP+kOt3+UgDA/hR11v6g9lj/qe39/8tWzf/M6Y//iDGe/5OJAP/Obzz+2IHC/sXdBP/Odnj/1T4zAL1tdP/uQ8j+Tjj3/kZ/Ov8D287+DwaA/xoRRgD2Kj4AlRj4/0eQmP9x/iL/FpsQ/9vgpv9UgoL/BhCp/5CnTgBaStMAZuqrAN0UWQB3XBMAM9u1/7bEbv+hRK//2QFFALHqWgBpRo8AnO3jANQFif/U9sr+yib7/7XNUQDifUQAFxayADXO7QAcq7gA1WeEAICEYACCxkkAQDWXAE2kEwHxM8AAX9l+AHau8wBg4fUAQgvuAGrD+AA6T9gA/VzDALkczgC5bUUBaP0qAdt3PwFKeRkBvHezAFqvmwCegmsAmFc4AT53wgFsCUYBgZr9AJfHOgH+aJoAvEgvALvjwADLTb0AtmhlAPg81gCzECoBote3AGURwQB/QsIACsV7APTUgABvjHsAij21AK8ReQBkYJQALRy+AIo5mwAV5nkAPf5jACdclQAU83gAFErVAEIqyACzJXoAZ9SsAAy3fADJa/H/3TtyAEmbngB5cS0Ag95NAKYqVwChHhQADefC/38Yz/975Y7/VGl6/3j75/8aoAAANPKU/zPoYf9tPar/z/ty/z6+RP/ByWr/ufqo/yMmmP8qfNj/oxLu/209cf/EFnT/1hMx/4G35P4hM0D/73HK/7fE4/86hM7/ceys/79Fff8XVgz/3Ljh/tmiGf9rOnb/sn2A/wNF6v/NMikAvfVt/9lpQ/9ADT3/cLz0/jZYGf/pC1v/G7xh/0bBjf+Iv9H/b+aw/ydelv9VCXL/v2IQ/0VqX/8Wb8z/4dEGAP7XKwA6py8AIhnr/9CMsP/b0oz/gOD1/0e2hADAG60AMRXWAFcxzAC8FogA/z9QAIMjVQBJ4EAASaReAB71fAAbN3gA4u5vAIVUnQCEgYYA8pdMABLbMwBSuSgAk2HX/zHc3//cykwAgdYrAMNZCgAhfgIAKfTB//cFd/82PtX/FsQsAHhmHQDIqxEA2MDq/0RUhP/YRzb/ERw2//85lv9XvsP/GtLF/zhvxf+wKTz/IYzV/lCKsf40sJr+euWW/q3N2P5mwO/+hL+i/iB+l/5cOoz+EGtE/j/fyv3UYUH9QCHa/Ltb7Pybjxr9qP5T/VEDZf2G0CP9bT/S/ImQkfzNWof8gItd/JUgsfwVKuz8/Do5/UAK3P0ybgX+8gMR/mGMNf4srhX+545F/oEM9P5ad4D/yMANAB1ozgCtAnkBrXPZARu+QAJg38ACt3VDA22w9wNvYkgEenETBF0BIgSi4lIE6p1zBPknqgTtNw8F3WYPBZd++AQwdawEqKNXBP1WEwSsvNQDxVGxA4yrVwMNowYD2Y0VA4csNwOOWP8ChtHrAjM11QJUBmYCBe4JAvaDAwJlvQICbRkQAhxUBgI/vdwBMzFNAVNoyQDrfZEAg8JgAI1RPQDfN0gA/cPu/7nbUv8kFB3/0DKX/j2KDP7lRBP+Eprb/XM0n/3mRZT90VF7/SHvd/3rC0P9X09N/ZL9RP3wSOn80bVc/CLKNvyEcUL81Nyz/J8i2PycYVr8xjUm/CreHfwTKgH8aNGN+5Mbhfsa1IP73u9S+22nr/qsMi36r0pl+h/Tfvswvoz8qZFH/KReoPu66Rj7yEIt+/1nw/sm/l38HA7M/NdtOP00uLX9xFt4/uNe9P93QS4Cdc82BOsXYwQQbg8D+210AsHK8AM/TH8GIuHlB51FggcAdn4GegXtBZAY/wUtwz4GIQMvBoH5AwbeYX8F+X1IBIJ+MQNXbckCVQ3gArrK9wKn8KsCiCjIAZIUlQESta0BfgW3AdgVCQIDQEYCorSuAlE+JQPxhFgDsAgqAwhZZAPnL/8DapFiBMM10wPRAokDH/wGA4BivAJQVggDjO6PAqBmrwFAmd4A4GBoAE+3tf+YKPr+4SWS/gDL1/4ITTD/l5E5/5tE7f66q9L+7bwY/1vKgf9lVs//UoGx/2kox/9y/AoAHR4VAJKnMgDCKkkAPd80AIny5f+BhVf/nrV5/q2N2/1z76L97PZf/TWqyvxUJYz7xI7p+hbazvqZeNz6DIy3+pN4ovqF5G76CbMN+kb7nvmkGGf5i0jY+VtDbPqooAr7HCAa+1XouPrDczX65ZpK+tPAofrJ9tz6l9GX+tKc4vk49WX5pmud+Q5gpPrrTlb7Aaum+7oCvvtLX/X79DvT/Cysp/0ym0L+XGzc/pHvx/83iDwB1tQXAwS1mAWbPBcJUQviDNuD7A2mv+4K2GdgBbg+KwH8EQsBXaAzBL4V3wfdFI0Jsye2CKUpOAbjRPMDmwaFAoXdBwIOFJwBZ8tzAIS54v5BzIf+zzhvAALvpgPSKqAGb2zmB4Fy7Qb7DSsFxx/TA4DFLAMG4ZwDgd3FBNyB2gV/uzsGIfPjBWRc2ASHDuIDcnoBA9E5qQGUO23/Lfe6/FIc0foOD4D6yAzD+70jnv1GSQ//CXc0/+OMMv6Enqv8Fj3u+7WQrfy+TW3+JI+PADNqKgL1lvoCnELBA69fpQR7IuUEZ0X4A3pZIgKzoSoAraJc/uF+Ff3eP4T8274T/ZiLyv1i1i/9uumy+wUXOfo5Ypn57TKV+aPLifmRoX75Rd2U+T0XgvrY0hX84xDQ/WA2mP9oU88Avy7WAPS61P8JLuH+IKQ6/sh8N/4ivMn+H9Zi/7vzd/876Q//kFZg/tvF4f1jX0z9ZAh0/Ed9oftzvvf6QSHg+osmH/vOGdT7qxsu/XRfZv5J4AH+4CMq/tU79/57t7X+UYSH/q7pPP5S/dH9b6xE/g0a1P5KmBf/9x6X/7zu/f8y6b//AzNx/2Zhwf5elQn/8HFoAGoM2AFI/lAE2slXB7C/5gq5UBMNWtPkC/T5Rgf9c/cB29X1/jo3JAAAy4AC/fHRBbIefwcIMWAFrkUKBAApJAIrOUoBwaY5AUhLUP/Wqt38brna+j7F1vpV5fT9vbcVArlEyAU+UJ0HKLomB0ub+wUGAn0ExChyAw7fVgNxMJ0DAjyOA5slpALlyQUCpXNSAmORzQKner4CdIr1ADhYr/3jdCr6BhWc98cjFvc2HBn4n8lY+ggrl/zrpYf91sby/VwFS/7KG3L/KiLRAG4xrAEBpJgBJkD2ALn5zADhLlABuGdTAqMGhQN0+D8EdU14A/1fbgF05bL+6vTF/OGYkvuEKHL7yEnS+6LxhPuN6sz7c1VX/KL6h/1tAb/+PLx4//CVkv/pvSX/E6Ht/mHJUP9X6lIA2hWrAcqT4AJu+RYDSLWEAq9jEAHA89//v1Ij/3KeU/7DhIL9IMTE/KPucvwJA8n800lL/Xn1tv2TsXj+sz/U/jvy1P6Wm7T+bp6x/pZjvP4gIBn/N2Cf/819xP8yaKT/k4pc/675Wf8GSkn/QbDR/mKtyP11WOb8Z0ue/Cix4fyWDlz9Eih0/TLZSf0TquP8kxFz/D9GR/xy4ab8DVH//COTAf0yL1T9cmFE/uYomwDedU4EJiD8Bx3/vQor2CMMeKTDC85fiAlnHMQFliPFAUN07P5tL8/9MlpY/stJrf+14cgAkOKNAb9F+QFDDK8B/pbUAPeJTf8t7zn9lb59+8F26frFIiX881ZY/2IrDQPsomQGjgh+CJ8cOwkZBFAJS0tcCPTp3gamPuUE7TVpAjnHSQAoryz/ccA3/wC5BQDVXtcAu0giAS5fVwBcuJ3+D3zE/DJTSfsRhrT6+Kcc+0qoZPy6Jij+gNO//+POdwGdX4sDpkpHBVhmFAa+tncFKFLBA1rdAgIJs+IArthyAIpwgQB3vc0AxGalAOjE7P/0dev+HlwD/uQrfv0pCPL8Wr8C/AeM5vpyASn60+l/+kCNFPx/yDb+aeQ9AEMQigFX+B0C3dAgAjb33AFYUk8BbrC/ABvmMQC7oVL/gVKX/lDLO/6KTWH+c+uv/k4zz/5Pu0P+pjt0/fXFnvyUR/b7bmjb+x28Evw22p38MilJ/cA5KP4L0BX/WCHr/9yvngDb7OYAacBYANPti/+49sL+IVLh/RoQQ/2iraL8iGRx/O5as/wdu978hL7V/CpAh/zeYkf8GaVA/EQ4HPy49Qv8J/8V/JQPGfxgC5n8VmVV/cMf7f0jeZn+ZitW/yySoP/DK93/ymZZAI8DPwH+uuYCKR6zBNT8+gVa1tkGPUZ9Bwvemgc6lbEGMhg8BJ2qAAHafiD+c4G2/HD7+/wSny3+hOaQ/whPywAVtbUBveNMAvx74AJgE0YDofkVA6ehYALYlLgBf9+EAR6MXwICZDoEuI1ZBlCEBQi8r+cI3JP7CBwUXAhKQxwHxYBJBdBvBAPF8aEApfG8/g2sdP3KKw392PB//fU4jP5PtZn/8BYiAAoBUAC4jTUAky8UAG888v/srMf/3cLL/+RzGABfRMgA0Ai9Ach27QIe3CwEN1ubBIXAGQRCLOYCKZcoAXRgqf+MU2/+IQwS/cQ19fs2Plr7cW0t+9YjoftyIcr8XZfc/aZecf6fvMX+h6J9/ofiNP62LGr+LYTe/inoSv/aU6z/yX07APoVrQCqbjIB8tevAVeGsAGAcQAB5OXW/8QAgf5vE4r91/IC/feeyPx4wcr8NEW8/Fvf3PxWOCb91pPI/e7Yov64lBL/cKAL/6Wnk/5EHLz9Ahov/YvCQf1ak8X9nGdJ/giVpv7Z6LD+kTpF/obh9f050/L9Tw2n/VifDf3081D8e6eN+23za/ux9sH7BeFF/MtuEv1L1KP9h6vf/VWwVf4ihur+ZHsw/xe0bP96Izf/cJWj/p99cP4sn0L+JZQW/nA3HP43q1v+Io95/s+T0/4v7j//Zgvz/4c2FQHseF0CSKOKA/+grwR8yhkG9o01B4nzjAfdA3oGTgrTBOjlDgNCPu8BSpT3Ab6XeQJXU9gCtx02A7RMgwM3mMwDDBtWBOZ1lARdxh4ETtQlA818/QEcrBYB4k4MAUYtuQHDge0CvP8uBHXaVwWhwhIG69djBiOSNgZ0DFQF+z7gA+ur9wEFR+3/ZIRX/ldOnP1Oiqf9AsJx/i1HW/+VwPT/BVcgANB+0f+wnX//RxMc/34Jw/7ImYf+cV+I/uzrF/9a0wgAP0IVAe/f+AFCgaACIdm3AoI2UwKA1kUB6kTe/5UVg/6sfZn9qhwB/WauivzFW2r8Msd8/A+Tzfy51Df9GIKw/dgRBf6Amkv+j39U/uiUb/4wI6T+DSwJ/4flf/92IMb/HA0PANo7LQAQyUsAJthhAKStSwBHk8v/dAw8/5+ygf4CcNH9EF+F/fqjRv3g/QL9uR7L/G53z/ydZuX81rMe/RRfUv1sFYX9azu4/Q306/1r0gz++xcU/mi5HP5YBDH+2SA1/pURNf6Zaoz+agTi/ndNVv+zycf/KH7j//82i/84P+z+pHgk/j2TO/1ghLr810eG/MscbfyfM2/86J+0/DuXVv3GTTX+wxHg/mUtNv9gW0z/A4UI/6lxuP6EzGr+Fm8f/p7sI/4JzLD+szdc/05XAQBrWoMAguKdAKP9kADTfKoAJj/4AGKdmAGwh2MCMCkIA2VExQMb2vwE6CNaBqbZQQd5hlYHg9JCBnrgowQmQAwDvgHuAWN7WgHXPysBljcrAeGeXQFFntcBPmuBAlR2UwP/3dQD1py6A6ZoGQN+d2kCnnDuARSWDQJK9JUCmCU+A2xh/wOvlZMESHIDBQGcTAWAQiUFL1iSBKXGpAOhoSwCI1GRAHAmIv8f4hf+e0yQ/WOnmf2I/Aj+yA+v/ikwjv8YAVsAzCPMAFjOvgARSVYANorP/+7Puv+tUdb/NKv3/1HNGwCFlF4APgfGAG7XFwGaJFIBSmQjAcUbtABdmcH/tgqZ/pletf1ZgTf9n/0R/aHlOv0V2bD9VAJE/s1GCv8LX8j/5OFYAGhqsAA1050AbaAMAMZtXP/IpMj+KhZd/oKXQv6uLEb+92NE/k8MUf5aOl7+0uRO/sLzJf6AI8f9GVde/Vai7fyyemz83EUO/CJB1vstDgf8TCqG/DM2L/06Qv39cYqv/kfPIf9r6Tj/CtcT/6wRuP5X4BX+Qdyq/eGOVP17c/T8KIbn/K+W8/yjf+H8qoUT/an8Q/2ANkP9mUdE/Z6gLf3tTBX9W6Nt/Tz4Bf4dJhH+bjsB/sbsK/5ubWD+eTd2/u3JfP6fq8T+Lyle//wGyf88883/yhm//2L31/+YIw8AUA4xAMCO6f8r6mD/ldgL/7NWqP4Mqon+nRMB/46EjP+heWIApkKnAYEn5QJ5BAwEtooqBSnB7QWPwIIGvz0IB/MFFQfegHoGKdR+BX19ZgR2UoUDlT5XAwRPbgMYRa0D4SPjA7em7AMVJPADrMnuA10ytQOiXVoD8vXyAhy6dQIurkgCMvdfAsyxoQIPMi8D4UbaA8o9YwQIIc4EDdzQBGw/dQTrL84Dy/7oApkf1QHurbwAGbPO/2NPEf+cHJ7+KIhz/uz5pv69bzf/hg7Q/y6jGQAONCAAZrfn/0fAp/+WT4v/YxuR/9Ihtv8L0sP/JTXT//ywKgB2/NkAdD5kAVHxWQFCZeAAQ6gqADoWSf8NeED+EdhX/SLz0vzCgan8UUG3/Mgb3vy+Pjr9VFPM/T9TV/6d36f+Jcqk/kxNaP64U//9Fd14/S9wMf2hDy39jZ5g/fpHtP1qQOv9DXYJ/qjjQf4pe3D+oTFn/oXKOf7vrMb9sxUe/W0AavwnLAH8QT7Y+4NKCvxoCHj8EWgT/aygzv0rKF3+cUrE/mE4yP5Nd5H+RCI2/j6Etv0gqhv9AE6l/G45ePwFS2X8TQil/HimLP0Dzc397ruI/t7jR//lPNn/NJ0uAOjVNgBj6qf//bn8/j+rYP7b3+/9liLp/QcBTv7ZisT+tSAz/xk3sv8ZqjQApdqvAGXuygBjg6QAmHUoABnVgf+exrf+52JZ/reZYP6Twp7+I6BC/6rO1v8XTFgAwd36AN2mxQFSbVMCI8jiApEMfAM23QsE2FedBFt4MwVAccYFsXaOBjijaAf108MH7cJ+B2J0bgblhBYF0UzLA5YnsgKlbOoB+DR0AcD0JgGphyEB94mRAeI/LgLdCP4C1JDBA9op/QP9Xs4D5NaCA+cCLwPm5QQDtMwZA7CbQgPo42ADXjmIAzaengPSIaEDGft/AydeEwOWKjkCmJkCASJCmf9IrVP+VleI/VvCLP2vW0z97nqA/dxmtP2phxb+NjJu/m1VxP4EPyv/n+6E/wCUtf92Vp3/Ne5J/2LgMf9hIqT/dGYkAGqJRQDwuzIAJmIKAD81yP+Q61//yS7n/g9dj/7/ASn+soyE/eX31PzSNn/8dxyw/JggPP2yHsz9wucY/tcRVv7+sKD+o5HG/hGYwf7kyr7+IUeZ/ntLV/4zjRH+G4rD/RBApv17Q8/9u8IN/mbIHv50uwr+W0PH/QEucP2XZyP9zLfk/NDstPwFXYv8iUx6/FW/qfwjkhn9Tg2l/XxXXP5AhQz/ZO53/zXZl//qy4n/Wtxj/0P/Ff8tMKr+CFc7/nQ2/f1ijf/9YpUW/rAoOv6RWIv+Ihb9/tsmOP88dS3/ycbt/sqxo/7L4ln+++IN/rzM9P0Lvhf+zcF2/qMAEP8ustT/VKmDAN4xBgH3rU8BSykpAQUNtgDF6zwAZwml/31GJv8J4w7/6chY/7CP2v+vR4sAs0wuAXCjiAHUoKABLuF6AUfOMwGG5hYBHll4AeI3HAJREwIDQE8IBCzgEAVZJCoGixRSBw6LLQgU4kQI8JqGB3MQGwbG22gE8+jXAnSHuAGcmhoBtLP7APNeOwHdys4BiIybAoIXeAPSNx8ERVI+BCWv2QO84CsDMHVvAg/r0wEPaHEB9vlZAYXmkAG8DQkCGsuQAmEW/AKXQEgDWnVTA5E15wLL+ggC7IzaADkvev9n5k7+eTeD/TOBKP2mNzj9MW2h/V3ZO/6mP9b+TS9W/7yGlf+AgJj/X1RX/z9D6/6tE2D+Mk4B/jHLK/41HMD+9XM6/3vyeP/RJbD/6VLv/6jhCAB3Qqb/nGPa/l+M7f1l/yz93YSN/Ha0Gvz1yjP8Qf7J/O03g/09JA7+/INq/hvxt/4x8+b+zBrY/kWwZv4um9D9DPpa/bbk/PzuOrX81IbN/E0xPf1J27z9ns0x/h+1cP5U8Gj+lZND/iNUDP5ZQab9p9s4/b7F4/zHqK38waTR/HqHR/0vzMv91ceO/ppLVv+v3MP/03/P//AGmv91CCz/I/6y/rMZWP6j/AH+SG7w/Sj8Gv5v/Wr+2Jf9/plSiP9cAq3/fGC3/3akqP8i3G7/zKdY/1HYKv/PaQn/HSpX//qp2//77TUApTOzACo1PwHjM1UBiokkAZzytgDlpxUAitWF/2SpWf/mil//pAiv/7v+RQD2ZdcAYu8oAUYFQgE4zzIBPgDlAEHwlAAqE1kAweJgALyUwgANh8YBYvYJA+NKQgT/NocF83THBtHHvgd5phkIWLa3Byc/cQZQhrQEgMUcA40/3QFJ6QMBh4/DAIcG6wAdBFcBImMWAgnK6QIU1Y8DBhfiAy7NrANvw+UCkbP0AfNXGgGCS38A6nxcACJ2pQAPmk0Btf8kAmRA/QJNtqcDz6j2A4yM0AO8qCoDKmX1AWmJbgBGPvD+Oqa2/UTW/fx3xrz8BSTa/JGfNf1Nks79B4d7/or/E/8Dwmn/5xZE/7oHu/6Q4Sf+xYO6/WNBhv0ymb/9cU1a/m50Jv8Vmv7/pCGqAAJwGQEX3jABjc3WAKEuBQBglu3+x8Ph/VJmB/2lDYX8L2hM/Hh5dvwlv+j8hmRt/XiJ1v36rR3+1P1A/gnPKv7AJNP9b8pU/ZbFDv1i0AT9Di05/VK8tP0q4k7+XDH0/mXxh/+ICNf/9sjY/wA1nP/gqSb/iYiE/nNH6v39S3X9XTY5/X++Pf1TYG79hpLG/W3JT/5gT8f+9LMN/5pSNv8GWzH/ngkA/2XIwf6aNZD+/k9q/tFNj/56zQb/Ugyn/64AVgAe39IAFqf2AC3gGwFCzAcBDZx6AH/f2//1zWX/6ov8/o+Uof4J/47+LsSp/r6TGf/FwcT/oetnAM2Y7gB5By8B4XgGAS/aoADOKjoALb3M/00Ffv/jZTj/+Ukz/8V7cv9UYMf//wxCAOAPwQDrnxQByFpVASqWzQEhgj0C5Nm1As2TOQPXPrMDBiVqBKeYjAU5044GFgvrBljjhQZPe2cFJRMMBHlS5AIJ/QkCXlNmAWxbEAGJYuoACTEVASNdpQH5t2MCIx8aAy0OeQP5i10Dmy/vAoueZQIG2uUBu7SQAUKKcgFKCoYBDZG8AZ4hBQIhlkoCmxOGAsU7qwKA1YICcqnyAaLR+QCcnMP/6lKt/oIk7/2N9aP9xjig/f7rwv2ZvPX9E34s/g/Fb/4y/LH+fVrT/kGTwP6uxY7+1uRU/qR1Mv4LIj3+kYR4/vSk5P5o83j/1qrp/xgxAgB9K/X/wifH/7dvW/80tL3+gaMR/kxZif0rhy79uhbu/FwZ1PxN8xD9BH6X/QFCF/6MOmX+WWyN/s7knP7MSYj+lNNS/jnjD/5I4O/9IqPt/cDR//06Mi/+mWWH/quN/v4QMVX/SVNT/wHEEv8nGL7+DThR/mR8+v3KS7z9vdas/Z3zxv3wIwr+egJq/rpUyP5EaCX/SOZ//+IEuf8pTqP/s+Rc/ww99/4ByZH+749Q/swkVf68J5D+Pk8H/1Swg/+Z+df/TowoAPrfTwA7XkAAg5TX//3XQv9oP6P+wtAy/gc9Iv5kEV/+v8DB/i8/Qv8AqgAAkDmCAGjGwAC9srsAPQ9jAPAVtv+Umgf/ech0/ulPNP5doo/+hiUB/+D+iP9GeSsAmSesAIBU1wCZFOgAt/cOAf7OewEF/UQCg68IA/EJqgPPrIMEFKzQBe03JgfAnu0HJ+ydB1S6WAYZdpIEvhfHAhVffgGanrwArVV9AGF5pwCyVAgBvOGkAQB2jAKm3GwDr4fmAy3JywNrdCUDwZpUAtVrvAEmnnYBCPOGARZa9gF5uo8CQQQlAyEkmQOwi8wD9pKwA0wOPwNpNl0CyPIMAaLEhf9n+yn++TRX/fa+E/0lOEv97kjC/fyCP/5mTK7+POH5/jY1H/+TjAb/tQy8/uESRv5MOdf9C8W7/dDM5f2dzFb+voAT/81b2v/DNXIAdSa9AH+SowCiFToAJQiG/5TllP41EZ797h/u/Ak1jPw4L3r835HH/ER0Xv2PlSX+Ea3s/r5zbv+uR3n/nOsr/78Ao/6XZwf+RkyI/T/MQP0dzz39KYmR/QwiIP4I06z+N9Ex/4QKiP+LB3j/mm0W/9LPm/5bNQz+Ag2h/Wskhv2Qjqz9VMkM/nkwpf4rcED/McGW//dNyP9kt9L/8Zyl/4k4Zf8Mgjf/epD2/tsBwP70Xcn+zGvU/vZ02v5cXSH/I99t/wK/VP/Mgz//KEcy//VN+f6dwdb+OBn2/huOAv8ehSP/xh1o/8m1X/8tfV3/IHyG/3ICpv+AXKj/XnrT/8kd+P8GlBEAfX4nACAd8v+qMW//PKvf/igGmP7UqnX+ZI5w/stBpv7KzwD/5VhG//0ErP+RuIIAAoegAWe16QJ5uAcEAy6VBNsm/gSIl9oFS0TUBn7VTQeRt/sG5GWyBS2f9gNbwnUCenePAYebPwEDRmIBwI6TAekFpwFZINkBcQ5QAtZq6QIKDTsD7JkMA5VLdQLbBdoBOH6PAbvUxQE7elACfr8EA/UitwNRETYEbDRYBPX9MATWmsEDxBT+AkK16QFa6JMAaahL/wccYv7cmPz9F1sN/poJcP49geP+uDM8/4LXSf9/ahX/p+u+/sl5bP7kWzr+l9kO/tOG3P08mtL9DMgz/mFXH/+zWDsAHAAEAXo3CwFTcI0AVmILACnDkf90cQn/2Jlo/tuJ2v1I3X/9nRdz/Xt1k/0NYO/9uNpl/oVQkP6xY2D+IBkU/jkh8P28Dur9y5H4/TPqBf70zSn+MpeJ/v65AP+w2Vb/FBt2/4qkd//pmzj/xP+q/hZY/P0Mhmr9a9wo/RGoN/1hQHD9oQDB/f4DU/5XVOn+6QFM/zP3lf9U9br/LE2O/8OwF/9hoZv+jDce/qpX2f1J8fH9U/VN/uzBs/51LjH/RdGM/179nP+O1U7/Lg60/vRoDv4yQrP9hs7R/ReI8P0Rc03+dWgK/2Wh7v+3PUwAOVR4AJRYjwDEpmMAQmj4/9EvMP9wNlP+rwGs/b+mu/2yr/X9EItG/mU69f6rTM3/IM5GANqTYwAAwFsApAHi/8NEQ//gubT+1c2C/pHOIP8Mn7oAOquZAo+zNgRo1MsFI1laB3f0ughVZDAJXY1bCBJbLQYut2kDeWMVAQQtz//9+J//LABbAB9OlwE9ipUC7uJwA9IIIwS3e2gEd8ECBEAA9gI0o3MBAtNBAMUj+/+XgpkAFyPaAXvtVwPex7gEAbGdBXs+6gUhjI4FxaWUBNutCgO81DgBC7t5/0tbJf6L/JP9KkvD/UmOaf6MDSX/OqS2/+lUzf/gcn3/2oXh/sigGf4Dx2/9H6H8/Hsu6PwrtGX9fnmP/i5w+P9X2ysBgHOdAXhPQwFSSnwArdmu/yH37v6CUzT+NO+o/brvUf0DHWj9ynDu/bB9sv4zBj//T1BV/9am2v6VrAj+PNBW/XtI/Pyg+eX8pD4L/enKbf1Jifb9u0mh/tKGQP9SFLz/61bo/29upP+lMOr+m0k0/l61yf0UYZr9Guiy/WvzBf5Wp0z+elpZ/o4lbv6tV4T+nlSN/nAUhP7+NWP+oY5D/r2QY/7YMrj+6XkS/3xhdv9wZsn/pdbj/2bdwP+J/Xn/ZQsb/3Yzkv6BMxj+W3/Y/feQAf6rO2X+ZLjm/vGUhP8/fxgA5pF/AMUJcwAdfP3/6NA7/4Plgv70D8b9KCJI/aS3dP2xMGT+D8KB/0UohwAbx0YB5VRVATEKtADfWKn/53V+/gkOdP3jWBD96/Ep/Y3ymf1lI4T+nybj/xFl+wDmElwBJ2d8AdwgkgGQddsBEsicAgKabAP83/YDaELmBMNcZQbRIIkHdcoLCOzBqge/2QEGW4q1A4z38AE/FOMAic+XAMqXGQG8AbIBcqwfAsvQsgKwu3IDvO2/A/4uhQNLodgCSGbUAQGq+QB9DO8A4CitAcQcvwJk4gIEyuHrBCqIGgVq2cIEhxQlBGb9DAPCgawB2adgAKxWL/952XX+55Fn/vcB0/6XR2T/eZrk/73w+/8fuor/TzDG/ttTAf7r7Wn9fOcE/cEe9/y1F1/9jHRI/hOSdf/bwpEAPh9BATkVegFFaEsBeHWsAGGnv//OMMf+oP4T/iPZw/0o8NT9Tbok/pwtl/7AMvP+Zzf6/kr6mf4Cw/n9a5Je/Ysc5/xceMf8t4bw/PP3X/2jrAD+jaO1/oKrXP8OVcT/xeXP/7j6Y/+y97P+OGwN/puGnP2Uk2H9oahU/QqGUP2kan39QPnB/YQm+f0e8xz+eplH/rpcTv5BtBf+QEII/o+2Lv68bGz+n7KY/mGEuP5Qytn+PIwd//5IVv8EizT/9aUB/0rqDP+eTCD/IeL8/gX07v5dGhD/6nUl/74GFP+7wLv+sJNW/nMbQf6RUo7+3jec/oFQlv4L0gb//9PD/2wTMABQkkoABVA9ADKez/8P9kL/Pu2w/s2ta/67J47+HkAp/59Twv9ARQwAS3YlAHewGQBi0eT/+ZB3/wyjMv+bRFv/TlAvAJgJigEumB8DaZtzBGv/YAVZ5jAG2rX7BsVcfwfoY1cHlg1vBgpw0gRX8jQDfDw9AgNJGgK/3ogCMzIgA2D9XwPFbx4DozXHApoTeQKaTh8C+LuPAa4e+QA0KZYAL6u9AMGUlgGSxdYC/hQGBMRNtgSXAcEEiaUmBPesTANB+X4C4VbbAV2qXQE5DAUBlVq0APQqaADaBEAA4R0VAIb/xv/ZQjz//eCA/qJ5r/0syxP9wzTb/PpdBv0cd4H9IzkQ/q01gf5fouX+6b5y/1kPGwDjYYUA3295AG/DGQCC4rz/wAOQ/1lli//Ymoj/ldNv/+B8Tf+sCg//djS5/jQaUv5Tsuj9LnR6/QNjEv01A8j8vVXG/PBmHv3phKf9j6EV/vDORv51eW7+PfST/mK5oP5zWZ/+orKW/sj/f/7oxUP+j8gB/qho3v33odL9obrm/ZPi6/2Q+Lf9mOx2/W+Ni/3nxNj9Z1Ir/kQ4b/4P93b+qqpg/omqZf6fSGT+xbJW/ug8fv5fiK/+Lg+p/pk4pf7Vsen+M6lG/44+jv/DDbL/biyB/8rY4v4sACb+mVei/cxSdf2zkfT9tIGx/hwG/f4lm17/rl8sAI1IngDmXU4APdjX//mDHf839wj+AEZ2/QtE+f2AQAX/SpH1/5i3jgDCHIIAMxhVAB7MiABAOFkA32ct/wEHOP5tsTz+88Lv/jI7dAALbX8CiF4UBBp4/gR9q7IFsh8rBpJQjAb0fdoGnk5YBnAqwATRPUgDaEjzAs1bYAMh3QQE2UB2BG3IOAQ4wXUDez/gAh4TXwKmWK4B8JkdAQh9pgDHJk8ASIqcAKVqvAHT1PQCvxnRA/80GQQvv9QDLGNxA/yNRQPJhSgD3Y/SAoYbggKaTjUCsefWAS9deAEssikBOPynALNT8/88Tin/L+Fm/izC4f0rZ739cJrV/bju2/1Bqtv9OMLY/XGc6/2UdCb+Z0l6/g/Jtv57b+n+hRV1/3cmaAB5Ty0Bbpc5AVR7pgDtjvD/f+x1/+f1HP/C1sr+wgma/lfva/7JEhr+SAjA/VbElv3w8579MgSC/YQ/Fv1MjM/8z4QZ/Qdjy/12tmf+Jq7C/l0J0f6VfLX+HiWA/m1tMP6+/Q/+Iqch/iMUKP4BRu39/eGl/fZefv12Y5f9fCS5/dGZwP2wu6T9Q12I/ZWLkv3lubv9Sj0R/tj6Xf5EHHr+P0Jg/gGlbP4Kib3+J2Mx/w8DeP94gYL/HgFt/3pgGP89+8H+dEGd/uOElP6SDHb+F9JD/qstA/4JdDz+v5Ej/0WMvv9/Wn3/rxir/uLCE/6NygP+vbZ4/tpV+P5rwV3/zjS5/7G27v8fYikAuM5CAOalWACmixAA2K9C/42rj/7wXJL+V9f2/ng+i/+0EHgAHiuSAeeB5gLo/SIEqrv2BMaEogVB3XYGLaPaBmSPWQaA2iMFiFjgA4PSRQPWiWwDjPA/BDdoLAVWQ5AFKIgUBclR2AM4h2MCFY1pAWPq/wBxPcYABmvCAOuq/QA/7ZIBUJhbAv0XBwM2aFoDw4paAxO+LQOakAwDj5IqA3HlhwPITPYDxw76A6gScgPn0JICDZOaARUguwAuHg0ASqyB/9lXDv/EvL3+U5xr/n2wKv41de/9aVW7/ZG2pP3Vfa/9n8bl/Y7cM/5YnaX+RY0e//Piof+SFQEA/PYhALqaMwDKAD8AgBwiACZcz//F5pH/86SF/50Ihv9f+TT/Gn6R/vHF6P12v4H9AXBP/RgAHv2d4wP9pDf8/AYJB/1L+R/9azhb/fSWuv38ZTb+t2uO/id6iP5BZEz+ad8k/hDVEv4g+vn9ozn8/Zdk9f27tO39E9HW/WC0uv2usJ/9GKKw/T6esf1ykZP9bqeh/d5Psf17fLz9oV29/RSC7f351DX+bUSd/tZZuf5GPcv+jXcQ/1tNPf9Kykf/dfg5/0ANSf+PQgz/M/3p/qH0xf5XXtD+h6Ou/qVXP/7UZAv+nioW/g8UqP7wu/7+Le55/2yNrP/h+YT/mcsp/20Msv4JwHn+dn2U/tbMS/+QH+H/DDlsAPEirQDPPI0AClLZ//woNP8Jngf/KGFx/9WtiQBM1ZoBzGGVAm3y/QIsFFoD6qDaAxzQ+ATp1lAGngvcBluSUwbuQwUFgZgTBBHv1gPs7acEu7OLBb8b/QXAXIUFKqtuBNWIQQMPAlMCCLsDArlasgGiv4AB/CVFAeRkYgEDYK0BL9ITAhLJOgLiTQUCs3fNARXhlwHJM9EB36BgAgH0LgO/658DnUagA+9h+QJOuwMC3i8xAXmakgAfxVcADZsmAOLb8v+gd2H/XJO7/htNH/6j4rz9P9SJ/dvyev0i0qv9RscG/oxTlP5Ni/r+K3U1/zcPLf99WSr/8x0u/xhpbv/1afH/fh5QACc5eAAUZkkA6q3m/2a2V/8WWM/+0pZN/p1X4v11m7z9BJ7T/QB6CP4qIP79/k6k/S8XMf0+bvb8G1/q/HGSEv2x02/9WmPf/ew+J/4nsBv+2gXp/bLQwv34/b39tkai/X+JcP33cVT9N2lq/f9ffv3x2HX9LiWQ/fgetP3OtpD9CvA3/UY+Bf3NWzT9pX6h/W+zCP4W+VX+0Ohy/n8TWP47z0T+HYlv/vb3yP7MnCD/kg6I/3ictf9t+33/Eaqt/usB0/0rQKX9mHO4/Ur1u/2lbQT+3Off/rGaZv/wZX//g7lB/61tE/+KBPf+6FCx/rJ0w/64FAb/4lli/6urmv/PBan/qBp1/3UbSv+WzS7/VfhF/6CfMACjNq8Brok2AxJs6wOc1A8EQ51xBFa6YQUzAy8GZGU2Bo1RdAWpVk0E6/DaA9h7YQRsrNsFXTVOB98UlAcF8jYGnMoFBHTJYgL10OwB9NNQAkcnuwL8JvICGCWkAlttNwJ/v9EBXvR3AXc+TAFaIzIBFxNDAeOLlwGwHVkCnHcuAyVItgP9LHUDGcGqAtxKxwFP9x8BpjUNAaPqYQFsk9QBMQ70AWEdeAHfw0EAa8fo/iOP3v3LO2/9BBGR/b2C8f2Xx2r+hn+W/sM4e/5Gj0f+bBQt/oC9Jv5fRC/+MkCK/lszYv/qM08A4ZW1AGN/mgAmMiIAxYWU/2oJIv9OEuj+aV4N/1yrZv8wiHP/vITu/niZKP5wE23972H8/A4wwPyZN738rhMk/fVju/2V9QD+jKDV/TzXe/32rRn9PAS4/FOth/zZLsz8mQ+Q/a6ASv7HGYL+e3Qp/qpksf2ExWf9sS1K/dI+WP2NyIb9Rlvw/fo0Ff7/w9f9gkNU/br7E/0VvzD9OIWN/TOn+f2OBmP+U0Pi/niZE//wZPb+aneJ/h0eMP4Fqwb+Pywn/kPWnP4OUl7/LKfU/2cKYP9r65L+x8IH/uVwJf6NM4H+8kHk/jOJNP84MEz/oNUa/2fIq/498KD+85Di/s4LTf/heF3/eFlY/zW5ff+gYtX/1fBxABLHaQFFp8ECM7bCAyBRbwRlQN4En4aSBa3qJwa1qyYG6smCBZBzuQR5K5MElmopBdFXWQariT4H2M1UB+dcNwa8M3UEf4n2ArMkcgIjdNkCOxtIA50IbgNGUxIDRICQAtes+wEgyJEB8wNHAVQzJwHcFk8B6UWkAeq0NgJuf6cC26jHAjyJUgJhB6sBuQYmAVgsBQFhbD8BYO2YARG8yAGlF3EBo5KxAEztw/8RniD/bRvc/r9p7P5E9Ab/Ep7w/pEBvv5SDIv+J7Ff/te9FP5PL879hPPe/W/Ugv4lx2r/JYMMAKdOFgD1W7j/s2Zb/w0AE//7+PX+CU8L/3noS/9VwXH/T6tP/7p2CP/sHMX+Es+E/sBID/5w3479mLJS/WxteP1cO8D9Zn/p/Qnb2P3nx5791mRt/c7tQ/3ieDL9OBFK/UXwlv1sm+j9F5bt/azflv2XyiL9h3AC/ZFHLv3la3D9cYij/XyGqP14jIj9TnpE/Uxb7/w1gNj8p/oE/SAGSv0AAoX9csCy/SLbzf1VrNv9ovzG/Te2p/3ug8n9rj/2/RPIUf7B6bv+87fg/kxLoP50wFP++oEm/o1rJf7r2k/+CXm3/i4oHf/Pm7v+0XYL/mG4tf14ROT9U4RU/hjnxv70Phz/zNQw/wPUGP85srD+j4iG/jVHBv/fw0kA88LnAa8JGAPuBckD2P0xBKLTBQWxrPEFhw90BqGCLQafSU4F/ud+BEs7vQT0E18GAilKCKNEPglZPB0IECilBbvgewNJZucCWynQA+BnJAUje64FnwzrBFJqbwM6XvkBeLRYAZM7bQGggNMByHMeApHtFgIrUxYCQuQvAqydPQLg+98B3+ooARJlfAA7x1oA84fSACPjlAHe1xICDWnAAXeWtgBlk1j/fZJq/k4mZf6KuSD/G4Tt/wdhJQAOorf/eHP9/uiyZf6fbTb+mAR1/hiTzf5vVhf/DwFu/1RKzf8qDPj/Qoif/8yJ+P5pxY7+qa6m/ncxFP8GKY7/LuXV/wVltP/AWyP/vmhz/idiDP5rYwz+CHs9/hCuR/4GUiX+iIYH/vZA6f3z06r91+hi/TNuNv1UNTD9eV9K/QGzff3XBbP9qr2v/TGySv3wFsr8IIGS/DDoyfy0UUf9s2Cm/VVvqf29EmT96psC/RFez/yD7uj8pCgI/ZEZE/0FeRL9lbEl/fKIav1YL6v9OsKi/aCoXv2Rwxv9zc/o/PmNFf2GaKL9m04c/i4aSv6LRzz+pK35/d/euP0O5MH9ZbTn/YVncv78wRT/lyNF/7XA9f7hTpf+StR3/o3Rp/4JDfH+ZsMP/0tpMf8CKRz/ibD0/iN53f5wyDP/sEz5/3riAAEJSyoCfCj3AoCuSwOVq4QDKeUZBJGC9QTAAtMFHqMZBii9vgWo5XYFstjFBeux2wYL3gMImp9hCIoylgf1cykGqusIBUH56QQm36UFizFEBrZ4FQZEiP8E4V+0A5qf0gIgmKICUTbdAhf4+gJ9eNMCZyh5AuJNGQI/GOABgae3AXbbagF3wAsBcA21AO1hkACQKbUA6KLjAG0T2ABc5mwAKl29/6/OHP8ultb+DjUT/90Vc/9TsbD/2XiR/3DAGP861oP+g2Yk/luAXf5tXzb/u5o3AMzpaABe4d3/T2VV/8e7Tv+eX43/Y6qN/3uvbf+6oon/x6jC/8ELm/9RqDj/zEDz/tTE2f5J6Y3+zcn7/b0s0/2fsCv+sG6F/gHRR/7M/579AxJK/Vteaf29gZL96ZyL/Y8QoP2Vu7r9x16X/enkJv0pCfr8yrAz/cvWaf3Hk1T9qbYD/e258Pw5Dhn9TvIX/TAo1/w3Y578zYaN/ExCrPwnadH8FlLz/FOEK/06AED9oWj5/BJPgvymyjf84uR2/CIbC/0y+2P9vC53/X74aP2nWCz95LTb/Jo+xfxHJRP9saB0/fwoqf0sR9/9ITUu/gTLTf7EnUT+f+Yq/lewM/4qnYD+SLLs/vNodP/yRL7/GSiw/60dbf+olkn/UfhP/2Tjiv/Cv0EAXAKHAdzR2ALvOI4Dkdm3A3EJngPblMsDAHBkBJ9y/QQKNFsF+mOPBdJx6wVnxY0GjOtfB/ib5QfCe64H/cfZBrGD9gUyccEFgW5ZBkWeKweQCD0HKt1SBuSo6AR3zL8DwmtUAxyjeQPubMsDptfNAwgPVwP+q4oCDDHRAUedXgHkDyYBl/MFAWTn1ADwbKoAmIeXABYKggC9e0YA5WzS/6w4R/9ppNn+AMm1/hS14v4sb0X/xY+d/+Eqnf8mwTX/TiKq/gwuVv6YOUv+3dmy/vYCdf/GUyIAxONUAG059v80H4D//2dH//RPPf+W0zP/urtd/zX0s/8l/N7/Cfam//73Ev++hX/+vDUy/mtRN/6x52f+9BSr/umQzv7yGrv+pYdx/tQ29f3f4Zr9XpGl/SwH6/3j2Qz+cwkJ/jqt//1iT+79Jay4/RfWXf3o4yv930M9/VmcVv1KBkf9dk4v/XKlNP0g5hz9PIvH/HqDffwl+XP82KGy/EKQ8vz5SuP8aQPM/ASS0PzIer78Laic/FgToPynxM385AYH/XyjEP07gdn85rPO/CCoAv1N9hj9dB7q/Dza2PxzCAH9Ft0s/SyDOP1ybFT9D3+4/Ryi/v13VOH9ZFSt/ZBR9/0dRZr+mzoh/8jDNv+JEQD/Wmfx/lDKAv9vkEP/LdIRAGrImAEvCRoDFzLBAxi4fAMpuy4DnYC0A28SqwRZ8k4F0yBbBdY6YwUwOsQFpiR4BpPnTQd8ad4H4ifPB9aCAQdgjg8Gj3bCBXkShQYitooHbDjBB0eN5gYKvY4F6KedBCnxOwS4LToEu6dZBGySYQQ6+gYEhCY3AyI1PwInxZYBVd9iATwuRgFuT/kAOMyTAOthUgCbqC8AfVLp//lCcf+tAvr+iC2V/q3uXP5k3Ff+zFqO/lBS3P6Nu+L+VmmQ/odvO/7U9x/+YR1B/s59nv6oEiv/gpvB/6hmCgCA8uP/mbKm/1kNkf+bmKH/JXa+/yigzv86PvT/YMcgAJMAEwBeLbz/amxF/39t7P5Sk9D+eA3O/kYA2P7cB+P+oDfN/kN9gv6mJgP+4run/WGmsv2ZHej9vpz+/Tl37v0Lud39IwbJ/bsLmP11/FD9vekh/cTEIf2zOj39yqNW/URVXf2giF/9R0RD/RkABP32L8H8+buw/COA4vyCAzv9xvWA/Rnhf/1hpUj9bRr3/GPGmvw9hmj8QzKF/MlL3/zBQDb989dL/bwxF/2Qw7f8Jglp/PViR/yCKW38d1XM/PXqNv1qk239zqtP/QNVKP1blUn9ugmX/bTRyv33dfn9cX5Q/qkS1/4UoED//4ZX/4KQXP8Jy4H/DEe+/xJ37P8PIA0AjkkxAHA2dACdzxgBtA9BAkgwjwNcX2AEuR1rBNaGCgSJm+wDpiFUBHgDBgVpIKMFb2cSBjt0dwb4QQAHJtCTB7E86AfTOdMHHe9RB5GOwAboI4IG3cCyBv+9HQcOV1kHptUPBxxZUAZoCWgFoTGoBOHWLwSyC+wDzJDMA1dhpQOaWE4DVLmxAndG6QFmFTcBPCyvACJmOwDzfM7/LWJ1/yoLQP8VFiD/Fa30/ii0m/7pIBr+jYyd/SbuVf39VGb9eZm8/X9EG/6zK1H+xUI//pw9AP5Ober9Tqoi/vR/mP4q5jX/kR/G/5SqBACSpNz/vJO3/9ft4/+gzR0AN80OAEKbvv9RLp3/+GjJ/2XD+P838gQA0VXw/9m0qf91YB//wjN7/nyJH/4unzf+joWF/nPzrv7K6nn+oj36/fP0ef23mjv9LJIw/QETNf21bUP9IDhP/cvXRP05Thn9h5/w/HdU5/wDu9v8+den/MJKcPw/G1/8GfOA/N7fv/y28ev8Euvv/F9K0PzhC6r8XJeT/Jaik/wTY7D84mju/G0wM/0JHE39AlUx/RWWBP1Wkfn8C/sM/TmvMf2vnGv9FSWX/V0Gmf2YwIv9P1m0/Szi/f3zojr+D81X/rQNb/4rzZ/+CdbA/gmH2f6FWwL/irlC/5NRb//M/4T/F4mr/yMU6P8ypygAObZFAClLOQCCoRQAXF8KAD2dUQCBkwUBBoPtAUIfnAL9APMC6TgbAx9GSQO7u5cDjVb6AxtCWAQd46kEBJ3+BKO2ZwWWIfMFU0eGBrxl7gZ1fSYHDiQ7B37vPwedeT4H1Jc0BwcXCwepwLsG2CNbBlHB+gXasr0Fk4qhBaBbjgVVXGgF6KgRBaOHggR9B84DMBMjA6wkkgLfLiACHJPMAVzQcgG4LAYB/oqMADMoFQDme6v/0tpH/wlm5P4m6oj+S0Y5/lZT9f2En8H9DDOW/dJkbv3Sk0n9g8su/QVtJ/1xQTX9ZkJS/fhbdf2y0o79amSh/Rzts/2Ef879m+n4/QkWK/4KHWX+GwWa/svZv/6A2NL+9XXf/vSQ8v7EghD/Kww4/4iZVv/wL2P/PBpY/3BwP/8CmSr/aTsc/73fD//fowD/0tfx/lAW4f7LVND+4yq//hT8pP7iJX/+YMpM/u+7GP4Bp+v9YEzN/Zkgvf1hd7b9TCSq/Y6rjf3k9F/95jIp/fIFA/04J/X8MaIA/aiRFf27zRj9cCYA/QeU3fw31sP8SB7E/FfN3Py/nPv8qbcd/cimN/1Fw0v9AXlp/cCClv0poMv9Y/z6/eVaIP6YJU3+Vz+T/huW4v4nFCX/xglX/zn9gv8cKbb/jBDl/8b4DADeXDAADSxHABPfVABm2lYAtvpWAPNpZQDvC38AhTqbAGQjqwBYhakA+1mgAOECjgBobYAArIyBAImljACqwZwAHFerAJcPxgCSqO8ADDYaAUhIMgGNhDgBDY5CAd3yXQHfk4wB5MTEAV2H+wG6XiICh6UxAiRYMAKmczAC13RGAt9TbwLF75kCAi+5AnaCzQKMx9sCyqvoAu13/AKMohQDhssoAzdoMgPzIjIDK140A+/ePAO1iDsDxforA6q9EgPDrgUD5pkOA8+DGgPexRgDyaD/Al0C2wJrWLoCA0udAgNYhQLBFHkCdf1xAmh9agImOVYChpE5AqWmGgIwT/oBYhjSARgvoQFseXUBGT9PASjXLQERSgEBteDNAGnllACK21UAhYIYAGXP5/+jj8L/VQmY/9YQXP/Ziwr/aD2//jKLj/4tI3P+VOBX/mY4OP5Vfw/+r+Pg/VkTrv2Y1YD9GWBm/W83VP0ALUj92lI+/daxMP0EpBz9m64I/X4V+vyipu78wa/d/EsMyPzE5b/8b/G//AQUw/x+sL78l2i5/Km/s/wiUab8KiSd/DKTnfy4Uav88OK+/Ag42PyVx/n8uTAb/c6sOf1XKVb9lth2/eKXqP3tKuL9YT8e/kEkVf5sroD+pbeq/s422v5CLA//3dRD/9j4c/+gU5//VwXI/2ci7v84OAQAitMIANnBAAAQT/3/9ekLAEuzJQDDTEUAJOtdAENabACRf2kAj2JhAD2WXwAcDGcATON2AMeAjADhtqUAe/vAAOg43QC+LvsAii4XAfoYKwG7UT0BH1pQAcEdYAEhLm0BjId4AQ5ygQFPtoABpuJyAeYrYwHxdFwBLLtiAVDeZQFtSFwB9FxFAQ0XKgHo2w8BsWj/AKz/+gCTYQEBxSUMAekdEwGI3RgBCJkgAU2YJwGX+ioBrD0uAaAZOAEyE00BEfFjAWXOewFvvpEBkJaoAdY3wQG0RdoB2ILyAdIoEgIV5zUCWBhQAgWoWAIE0FUCJeBZAqlUaQIWvYQCkDugAlopsgL6aLACl7CbAnJogwJweXICB/RsAm6SaALbxVkCbn45Aoe0CwK3FtIBP8KUAegyWQFqUR4B1zfuAGNivQADp4UAGwZFAPbrBwB6J9H/1CWV/34bVf87+BX/AnTi/h2Au/55K5b+AeFt/vUjQf4EUg7+vTHj/Sxmv/0Qn6b9FMaP/ZXref34yWj9JVJS/dyqOP3CtyH9akgN/YLo9vymQOD8FWjN/EQ+yPw81c38Wybe/OQz8Pw6Xf/85HEQ/Y7zK/0CK1P9Q6d5/VQioP2NXsP9MvHq/dVzGv4p+0j+KZF2/poaov5178j+qz/q/t4FC/8oqDP/7TFa/4oKef9q7ZH/j5Kj/59frv+Zu7P/B5S4/9Nev/9BPcj/hNLO/xtA1v9WdN3/V5Lp/+Gi+//UkxUAFlwwAPKqPQDClUMAM8lKABNsWwDAamwAh25+ANGFkAAacKAAwQ+vAH1yuADc5rkARu2yAOJIqACnL6EAdymhAJIVogAGz54AnlmSALmWggBx4XUAFJZrAHWhYgAB5FcAf9pLALVeQQBNjjMA+7knAGvgIQBRQiAA4ZwgAFCOGwBoaxIA7Y0NAP5oDAD1wQwAb/sIABtZ/P9G2fD/LQ/q/4X77f/0bfv/kDQHAGzZEQAr4xYAtEsWANrTFADOSxsAJekvAEW6TAAaHG4AwLqLAOW0owBNW7IAhLq8AGDSygCi4d8ANHD9ABITHgG9qjwBWclWAQ49ZQEai2oBy4ZuAUz3egH7NpgBXia6AZBe2gHYYO4BnFf0Acj37AGtot0BeZPPAeS90QF3jeABSc7tAXvE8wGGo+sBZS7cATMIxQFzyKwBP5+UAT8ygwFnmXMBgEhiARLyTAFFxjEBh3YQAS2R5ADNkbYA/2SHADC4XwBFykIAH0MvAHzzGACO+fX/h0TL/z/cov+dLoX/tChv/9SsXf+UW07/Hw4//7UGLv8WxR7/3C0X/wuNFf9vvRb/uW0X/yFhFv8ovBH/TWQP/2AjDf86ogv/w0YP/wz/D/8Lpwv/CVsC/xYB/v5utgP/gn0I//zEA/8a8/f+lBDn/sZW1v7md8r+3frG/kcczv443Nr+G6Dk/uLJ7P5r6PP+piz6/q3A+/5E2/f+0tL6/qMDCP/RdB3/szAz/yfFQv/MUkr/7BBG/5VVNv8t9yX/w3sb/+uyGv+kXB//BmYh/+8tHv+YbBX/ihsK//0gAf+eH/3+Qtz+/r81Bv8oSw//1cgY/wCrHv9lziX/0mMw/3P2PP9oLUv/fCRW/4BsXv9QgGL/SMpn/+Zubv9HA3P/oEp1/+Drdv9S/Hn/bHaD/zU9kf/oPJv/hvKj/56Gqv/GMLb/t/jF/3F/2P9Dg+r/r1z8/0bvDwCGQSMAv3s1AN54RQAHGFYAsi9jAAgjbQCtk3MAPhF6ALsfewDwj30AppuCAFL5iADv/YsA86iCAAfCdgADuWwAKl5vABLmdAB0WHcA6MB4AAXTeACTOnsAl2t8ADdafgAxYYYAvviNAIM0jgC6dpAAcVaYADYAqACARrkALeDDAH7ryAA8ds0ApLnUACoo3AC2CeQAGtPwAFR5AQFWbwwBbEEXAdGoKQHF4kUBOH5hAaY0dQEZ2IUBtfmYAdc/sAEDAMEB9wnPAa0L3AFnQ+oBovv7AcK1BQL+SgwC1sEPAgMHDAI0swMCaK34AdAX9AFrT+4BJFnjAYKf0AHd+rgBwZ6jAewFigEbmmwBySNMAc1rLgGDvg8BVWzvAPSq0ABVGq4AexCLAN7zaAAbIEkAqwItAPjpDQAHTuj/tpu//0E2m/+8DH//XpNh/5bYP//E1Rr/KVL5/o813P5vaML+bVSt/kcblf5H7nz+l0hl/rGqUv4thUT+3Z85/sXDKv6irRn+HKcN/og3Bv5GggT+2zcF/nSpBf5G/wH+f1EE/mkzBv6hbAr+XscR/mw7Gf5RuCH+PHoo/tQRM/7SVEL+vihR/pTuXP5t1Gb+gFFs/j8jdP6JS33++i+G/u6Ui/6MoI/+XHuV/oQqof5j3rD+dIDC/pBy0v5WuNv+gkLh/iFC5v5/vvD+XSUE/yYrH//Zdjn/CbNK/+dkUf+jCFn/0ttj/1RueP/nOYv/AWGX/2csoP9C4qX/MbGx/91zvv+DL8r/iTHR/3ql0//iPtX/Exnd/0cO6P+Y6/L/gnX3/25+9v+pdPb/Gnz7/1DjBwBc+hAAON0VAI4rEwBJZQoAPaMCAEZjAQD15gcAySIOAJH4DgC1NQwAhg4OAFMHEwDSgxoAfZsfAHsDIQDYgCcAje0uAE5lOwBAvUoAa+1ZAGT8ZgDL9nAAJTN+AFc7kAD76qIASsi1AKndxQBIBtAANx3bAMVZ6gCOnQUBlB8kAVBwPQFM4E4BdN9aARgpawEIUXoBTTuMAbuMpAHNzcABtWPeAXFE9wFcKQsCVowfApOINgJyVEoC40BdAo5AbwJRN4MCFlGVAkkMoAJB56UCx3+oAqOgqgLfeaoC7CCpAg7HogLS1pgCuvOLAl9pegIqfWMCH2VMAozCMwJ3bxICWxbsARlhxgGVQqYBHbWIAUvpZgFHJD8BRJgTAes75wAmu7wAM9eQAPNmZgDHkT0AFygWAA817v/37sD/8B6W/18kaf/tdz3/Ha4T//lu7/405dP+uJW2/jhWmP5Zsnb+xARY/tjxOv45ix7+DfcE/qku8v3DUun9khzk/Tuh3P2N+tL9PJrM/ab8x/2fRsX9SVHH/frb0f2kf+f9fY/+/eVjDv539xr+PU4o/vjHPP6K8Fb+ZEVx/o/li/5LIKT+F8O0/nk7wf7Tus3+Avnc/qeH8v7s/wf/ELgY/5xSKP90yDX/jrI//1nVRv+4v0r/BztL/+1vSv+mJUr/lFJJ/4fISv8lqkf/e0g9/3KlLv+gYh7/QgQV/7EtD//ifQX/HlL6/g0y7v53JOP+WS3f/p9V2/64O9r+4k3d/tGU4/59gu/+uFz5/uEKAv8Gvgv/9sMX/+NaJ/8erDn/4UdO/3C+af91DoX/Onie/wNItf8ouMr/LZPi/9oj+P8Tcg8AICYoACrXQgD9TVsANg1xAE5wgwCJvZMA2C+kAJNRtgBlZMgAsgnXANER5gAUwvQAQiQAAV7MCAHWshEBL+QcAYt7JAFdnSUBqUAnAVYPJgFsLCQBJcggAY4zHwE7zxwBftkRAcnACAEYewIBVVX+AEpf+wCqTPUAbDjxAMOq7gCgKeoAoNPjAK7y2wBQqdUAa+jVAJuL2wDzkuUAIsnzAEx3AgHdnA8BItkXAVmrIQGL3S8B1/dCARPrVwEI0G4B9nqEAcWolAHyUp4BBrKjAbtUqAHOTK4BpcC6AecoxwFHDNIBDD3XAdHu1AGpd8kBhVS8ATfFsAGxXqcB46KfASIdkgHr1nwBmPhiAUrLSAF2ny0BbYwVAVLH9gC9ENYAhR+0ALJAkwA8RHcA/WtaAK6oQQCqXCYA7iAKANvQ6f/hz8j/ccys/2Sikv9vdHn/znVa/078Nv9W1RX//aD6/nmZ5/6kSNj+7wbG/hsIsv7QhZ3+H66M/kpPgv49yHf+92ps/owHXv4m7Ev+oexC/itRQv4u80b+UTdJ/hrZQf7l2Df+bH4v/ogSLP7SPC/+A/E2/jrPPv5yzEP+gxpH/p8iTP6Ni1f+QOBk/pdDc/63vn7+C4iI/kndlf5Vf53+6RCm/lIQrf7xk7T+BS2+/u1YyP7oQtb+ETLj/i257/4y5vr+ee4A/wXOBf85aQz/PIIU/wceH/9Nbyz/llM//zNlTv/9+Fj/wxlh/w8caf8JJHD/A414/6SngP/I0Ij/c7KQ/97ilf91WaD/ft2t/0Viv/8kYM//B4Ha/2EI4/87Au3/q2X6/5GyBwDQjBEAfAQhAHdNLgCbdzcAor1AAGscQwBWekkAmUhQAN6eUwC7JVcAodVZAG+SWwDQc14AbJ1hAEObZADCj2MAkqhdACDGWADoSVMA0flRAB7JVgCxQ1YAsAJWANN6VACBolEAB2dRAM6fUQCxE1gATmtbALaKWQClnlcAAyxYAFm4XQAXRWMA6AxqAEDAcwCh33oAwCCAAJ8ogQDsRoQAPHCLAKxWlwDUdagAGiy+AOZz2AAmYvAADzUFAQH7FAF2jicB50pAAXQUXgH5dnwBiYGVAW/PqQHPOrsBqo3LARcj2wFTqecBG3vyAVn+/gF0RAoCrBIWAjvGGwKfdhsCQDwXAj3CEwLYAA8CPfYEAvOH/AHGqe4BL1XdAaZvwgFKWacBjquMAa2icgHWuVoB7AlAAZLrJAGFCQUBcUXnALc6xwC4jqcAU+qCAPBCWwDYeDUA5cMRAA+t8P/dWdL/bty1/+MGm/+9on3/WYRd/08IQv8MlCf/R4sP/wCU+P6VD+n+jVXf/sOb2v6voNP+HlbI/knfuv7bha3+wAWn/oZRpv4kBav+xC+t/t3HrP4llKv+UO6u/nxotf7m/8D+xFHJ/v2bzf5rctb+Cp3d/rOh5v7Sp+/+tl72/gN5+v6fx/3+tEoC/6q4B/8QcAz/0JgS/3odFv8ylhX/YK8T/+ZmEf+YvBL/dJ4W/4ARG/+RIRn/8YgV/0HHFf9lFBX/YjcU/wcVEv+KmA//fGEK/29RA/9aqf3+3ZH5/sEJ+P68Wfb+Jpj4/il1/f4nmQX/BQQP/+IwF/+aLh//i9Yj/0sqKP/tey7/swM7/yvvR/8dklL/TpRc/39iZf+7zHX/MFCH/+Remv9Hfar/XAK6/4N4zP84JNr/GBnn/5Rj8f82sPz/+gEGAEDTDgAWNBYAyY8fANMCKAAysS0A74ozAC65OgBeEEMAOwFHAMASUAAfglsAcrxjAPX6YgAknWEAKc9jAPeAZQAxM2UA70thAI9KYQCZrGIA2zFjAEgtYQDXZmAA3w9gAHhWWwDaaFYAtLhTANvbVgCqvloAVwxdABzfXQAdEl4AemdgAH4fZADM120Avlh3APZ5gAA+TYsA6zmYAFlEqACEurwAfRzSAMix6ABOVgEB5KcVAT/RKgF4LD4BF61SAdKBZwHYY3sB87+PAfn7ogFkXbIBgQLCATEw0QGUwN0BD5zpAa0b9AGiIv4Bv8YFAp+MDQJb9g8C/cUPAsZSDAL2egQCnbD4AUAD6gHdc9gBrxDFAc4IsAGwPpsBy46GATvQbwE0NVgB4o0+AbYtIwFEPwUByC3pANL3zQAj6rAAFI2PAAJ7agCjMUcAv9gmAN/OBgCiHub/g4HD/yZGov+O94L/BjFn/7GqTv+3Gzj/pigg/1q7BP+qJ+/+2cza/qXuwf6bGqr+tjGS/nJmf/7YY27+emlb/rpgTf7YOEX+gcZB/q2OPv4r6Dn+ewM6/vM2Pf62XUH+D2xH/oCGSv4vjUz+4ZtM/oNST/5tM1T+/+dZ/pjVY/6G1XH+Xe59/oGMif4qVZX+O/2e/tSdrf6rF73+YJzJ/n9Y1v6AyuD+mzTp/jet8f6V3/f+3uH8/hslBP+NAw7/xRoa/174Kf9gMzj/DuVC/z0+S//D6lX/Dtpk/5p4cv++CH3/lgCF/wKxiP+so4r/KB2Q/2yImf+nEKP/iaSo/30lrf9apbL/6jy3/6+Wvf9gs8f/z/XP/ytA1f9ywtf/kx7Y/wot2/+Ul+D/Hmnn/5Hw6v91Me7/plbx/zLL9P88R/v/yVgCAAVKBwD+xAgAzBULAFA0DQB+exMA0RUWAA66GQCf+hsApFYcAMH/GwBMqBwAYK8iAGgbJQDHMiwAh64uABpqMABSBzEAnq8xAN3IOAB6skEACbJMAP6FVABFpl4ATHRsAEumewCGdokACJGWAMmcoQDmvqsAaOm5ADuMyQA+99sACD3tAGgYAQGcURUBc/UpAagMPwG5VlIB/M9mAUQ1ewGKJZAB+kCgAWYwsAHf274BZj/KAVOZ1QEl+98BT2TnAZMF7wEMT/YBUIP8AfEiAQL6iAEC34AAAjqc+gEGifEBlVvoAfbm3AHNn9AB+pu/AcmspQH9aogBLoBpAfj3SQH6YCoBOlIKASff6gDcKM0AMditADTLjgAifm8ARlRLAIi4JwB31gMA8Wzl/2jQyP8nUa3/oFGW/9U+fP9kiWP/I5ZJ/7N5Mf+8sB//fKER/5mSBf/rTvz+oj3z/rBk6v4n1+L+TtXY/usR0P75Lcj+Dq7B/tJavP65FLr+MpO4/j+Jtv4tgrL+Opis/qqYqP46CKj+U46r/nbCsv52xLr+r6++/r8Kwv43f8L+HlfD/mHQxP7Ry8T+3uXG/uBoy/6has/+7LLU/rh22v4yN9/+rUfk/g3P6v4NCPL+wqv3/qXM/f7pYgL/X64K/4z+Dv+4XxP/l80X/7fiGf8DeyD/B3Ql/1XkLP+qRjf/LJpA/9tmSP9421D/PE9Z/wWqZP/gLXD/zq9+/4WYkP9FAqP/+0K0/7Zswf8Hm87/rIHb/9C/6f9TlPb/sKcEADq5EgB5lB8ACSUtAMFrOwASVUgAdytSAOWBWABNclsAS6FfAJOzYwDvvWoAZRFwAAAJcgBYAXQAdu9zAGgjdAD/8HMAq3NxAHB5cADcGG4Aw1NoAHu5YwB9il4Ay4BYAGVAUgBg/kkAYxlBAOcXOgAuzTQAB7UxAM1eLwA6OSoAda0lAKs2JQDAICUAf5MoAKc3LABVlS8AB0UyAKCGMQDEKDIAUWkzAKhYNwBKYj4AVYxFAA2VTABbWVMAEM5bAAjPZADTBW8A7sF7AJ8VhwDRtpEAHqSaAH32pQCUxLUA8gzHALyV2ADwSekAiib7AHHqDQG1cR0BlIYqAb8/NQHeWj8BYMxJAT3jUwG5DVwBOMVjAaMaagE0FG0BIGlvAeKsbgG7a24BETZsATcNaAHDzGMBDmVfAWbJWAEACFABAMlFAZV5NgHVKyUBTNMSAcYR/ABRG+UA7HDOAI3GtQD6epwASYaBAJYBZgADnkkAsKotAOUcEQBpgPX/8bnb/0rvwv9fgKr/602S/+kiev+SMGH/luxI/+x6Mv/7GR3/m3AH/0R09P5mEOb+TO3X/iQYyv7qesH++ka2/q9Urf4tuKT+yfub/p2mmP6yBJL+00mO/mTgi/6GPYn+e9WI/v/6iv4jBo/+ldmV/icsnv71H6f+qQOw/uCmt/4vbMH+JArJ/us/0v768Nz+4sDm/qrB8v5Nxv7+v1QL/2HLGf8NyCX/wfww/0arP//GgUz/pSdd/5iwav8G7nP/wI19/zbJgf+tPYf/+qyM/7THk/+8+Jz/+guo/8bhr/99ALb/ZEO+/9qaxP8MXMj/OzbN/9XN1v92RNz/7cPi/5h36/9XNvD/lwXz/1d09f/2HPn/vt/+/xTpCQBCIhIAJokYAA6hHACpISAAnf0kAIdZKwAnxi8AVUAvAIQ8MQDpQDAAtyAsAJ53KwA5+zEAicA1ADSiNwCvAzUAhLk0AI3gNgBxfDQARkA3APpFPgDJlEQASe5EAAVIRwCJEUgAbcBOAFaOUwBS7FsADeJhAM+RZwBz/ncAh/+AAFrbigCzoZIAGBylAPzQtgBUh8YAoQzTAAdE3QAPPeUA9CnpAOSo6gD1QPYAjYP/ANdjEQGwcB8BoG4zAeghOQEbwkEBeZlLAUZmYwGTk+gBxf+/Ae+EPAFg3zkB8NlRAWNaVgFWBGUBilJyAYrpaQGRtnUBCGJiAbHXVgEycVkB6iE/AeH6PwGLXzwBrnMiAaeDFgFodvEAYAnRAK1exABNq7AAJp2YAFs9fgBLYm0AINZYAGhbRAD3sjkA+53+/6xS1v+dAMP/UmiZ/xmzjP9D7Zf/kgSL/15lYf9wDUj/FyEn/3IWDv/6OP7+kFfv/v267v5ka+P+eUfX/qcCz/54lsH+JKC3/ue9tv4FRbv+Vny9/tD8sP57yKj+I1Cw/gAYtf44db7+Jrq6/h/l4/6i5wD/G6nQ/uhNov5b+rX+MSvw/iGJI/+4wVX/lNs4/5Ts4/7tbLb+ixbs/pb4Sf8GpVz/qCYy/yIWCv8NZf3+1cMQ//txJ/8BCSj/7b4p/y3zKf8Oggj/0Wrs/vSo6f6x++v+7UDv/v5x9f49Pu7+LCfR/leOw/4T2tP+dpXZ/sGy0P6pLtj+YNrP/o0Lv/4Ry6z+gjKy/shlzv7jYeX+XdXo/vl+1f4GA83+MaTP/tnR4v4KHAD/qlUW/+FhKP9lOCz/LPo5/yP1aP/Q55D/gWuf/3aSp/9ACbr/Nxra/7o2+v/1CBwAZFVAAIVCWwCm4XYAzR2bAPU3ywDjrPYAZV8aASy9PwFGNnUBIBu6AeS1+gGNbjECmV5fAkAAjgICAMoCoKkTA0s9XwMLy4MDJDR5AxDUcwPPXJQDSi/QA3OY6gPpW84DS/OaA85RhgMnBqADtF+7AyYmowME2FkDszUTA7wI9QK0wfACUPnLAjP9eALmIRUCQeDQAQ7ssAGmDYcBYgw8Aa+j4wAY2qQAOp6IAG8ldADZnEYAssv8/0ELt/9U34v/ft93/zXZYP88Kjj/Q3IJ/wpg5v5qbd7+6Rrw/kIeBP+grvn+gJnf/o3Y4f6Gmvv+VQwU//IyGv802wn/yVAC/+2wC/+WbB7/kQol/+q1Fv/uvAn/Y6YM/4xOGf8wTQ//Op3v/g27z/4HaLr+KhSs/u2Wj/7OZWb+jSNE/ieMJv5d9Qf+7+jk/VeXyv3tl7b9npOe/WIthP1XaHP98UZt/YaeX/1bIVP9fJdW/bbdXf0LrVX9n/la/d7gYf1KxU/9qjRk/SCdlv1U+KD9Q+qR/XBMpP3B/dL9nRb5/aDsC/5s+/39dgwG/i0FO/4ZlG3+fghk/m9AQ/6aXF3+kZGH/ikUn/4FLKv+LSqo/uoem/4G/KT+RTzj/qumL//Ld1T/kDJR/yAiWP97dID/mYa3/4pq4//Qj+7/SE/p/00m8v+8rB8AOPNdAMGbhgA/FpsAoW3eAECUVwEqhMMBqKsNAtWOSwJTv6gCQPwmAxBVngMC9QQE65lSBBtukAQSVM8ENzkZBXFRagUVz54FsZS4BcwCxAXyjMkFOfnWBXNH7QV8GwIGBUjxBZLTrwXxIHYFKoBXBS5SMQX1vuEEKl5wBOFm/QON1ZkD9i1GA0/P3QIhxVgCLajcARjFeAEcfCoBtJzUAH/JZgAOWPX/dsuS/zxsQ/+U1fn+cEm1/lIuZ/4jQf39yrWk/b86fv1yNWv9m8BD/bvsGv2A5An9oeAL/S75If0DQzn9kMRZ/S9je/3BE5b9ds/C/eaB/f1EQiP+4pBG/nT6pP670Rb/qxxI/0LqRf/jE3j/FKDu//E9MgBqGRMAohjV/8D90v/Awer/q7zh/7a5uP8OTnP/GYVM/xyoXf9iaHv/Bt1d/wEYBP9w28P+lTy2/o13ov5Tzlz+VTT8/dBLs/0dSX79pjdb/elZQf1xAh/9bqEH/T2KAv15RP/8w9D1/Bv25/wXD+b8I+/r/E2E2fwJjcL8Eo3N/IsM5fxUr+b8mU/a/L/h6fzokBn9GmlU/bkOi/0clo39EfSI/YHds/3WMv/9LhQr/ge+F/6aKgf+vwgm/qIaVf5hHWf+62xu/rpJhP4jAq/+HiPU/mBW1/6w/dX+MC/e/mvL3f4t9NT+w0ng/pHd8/6cPvP+o03w/naEBP+FW0T/zXJ8/1XLh/+9tJD/qF65/8SI/f98LU0ALZeKALurpwDZb8QA868cAdXtmQHb+/sBbVxIAkq1lwI/thADrhCsA0l7QgSSF7IEcNoGBSnTVgVjrL0Foas4Bkcvhga0apQGQH+WBnYTswZBa+UGiSwMBwa+/QYc6scGvfGlBgRapQYh1ZAGO3ZQBuiS7gXBCIwFCww2BapO1gQdcVoECAfBA9B/KgO6oKgC3X8sAlpTqgHjbiQB2mGsALT8NwAs1Mz/8MZy/wbtF/8SQ7/+Ewdu/umbI/6YiuH9uBOu/c0qhP3Ju2X9KvBJ/bTGHv1hiA39l2NK/QVClv0wG6T91W+O/eO0t/1TEj/+dp/G/lLG6v5ClNz+9YkW/1eNjf9ub+T/CRbt/4qt1f8hEN3/gEQOADSZJwCS0RMA2J75/2fK5//d99//ew3R/6dOs/9Oq4X/FnpT/3NEG/9Rd+P+Oxuv/kpuZP6L6gr+vDa+/Uardf2cySj99w3m/GNVtvy18ZD8flhb/OBAG/z6Pv778PYH/HjJCfzmuO37rBLI++AtyPvVa/T7ql8Z/HlrEvxkb//7oi0c/NDtZfyRmK38Y8fR/DOf4fyKFxj9/fxy/XGnwP2+aPX9wNcZ/pW6OP6RmGf+Lg6d/hEVt/6OQcr+zS3e/i0g5P7sU/H+EwIm/1gEW//lRVP/7nw1/0WzTv/0gIX/CuKF/9Xybv/PmX3/J7R7/+lcYf/11Xn/UFeo//R9nv8bLYn/cai2/w3O+/84QSIAP8Q0AK0TVwDzxIsAaPS8AHRn7AB+qTEBh817Ady9vAGgvRQCauOMAgWWBQNBfnQD8zD2A5IkhATfSvwEKSpYBdz3sQUgORQGhNZoBjXLmAaU07UGDt3ABnSsxQY8X90GS0zxBox03QZzObAGDOyMBjg7cQaA/UgGzWQCBsuyoAVYBjsFf13SBAdGYwS+8uUDKiJZA67uxgLm9T0CmVbIAd3lSQFwicQAjmlKAKS43f8X2XT/froR/99Euf7N9Wj+KqYJ/rSGpP0B8Fr9kg45/apNHv264+j8s96w/AvzqfzCo+L8vhUl/RpKO/3XxR/9jggv/YUruP0tqVv+4FOP/hg9bv7AyIP+bIcK/xo8k/9gLK//ggKD/wDeeP80Xq//nDD0/1emEABOTvH/DeSk/z69h/9NDbb/O8nO/yIijP+eLxv/D/nZ/gvwzf6JbsH+/4p8/l0iEv7IX7/9IYqT/d5/a/3mLDL9Xh/y/IWKw/ww4o/83HxQ/K80Nvy6d0H88vk8/Jz+Cvwh1dz7/tvw+5DTL/xU7Ez8pLdG/HIYSPxmN2b8ykap/FhQAP07ETT9CfMy/WnXR/1wgqj9SGMf/kMwWv6L+V3+ZXJv/mfqsP5lWAD//+4y/+3DSP+1wk3/CBZH/xPbVf9WeYj/9UCa/55Lc/9SAmD/o36A/5y/lv/U3Yj/tmyJ/z5Ymf+1+Iv/L5t5/9oRjv9qi6b/zF6a/wEbj/8re6f/vE/N/1af6/9pzQIAdGYUAHYzOAAUUnwA1Fa7AJPp2AA4H/4Am7FRATmZsgHT1w4CRaJvApv22QKC2lEDGFTHA0zKOwR6F7oEsnwrBahvgAVOb8cF0nwUBvmMYgZkJ5UGR7qgBki3pAYp470Gy7HXBlDt2gbSFb4GoFmMBtcaYQa8IzsGYCgABsKBnwWpqzAFswDABFwoTgTJ1dQDm51CAwLxqAJDCCICc7+rAaj3LwElj6kAuVEmALAqsf+Ogkr/cL3g/g6Edf7hph7+jvfO/YmQff1aGir9jKTl/EK3xvzcf7X8xACN/AZ1Y/w4DWr8QaSZ/HqMyPxKDt/82fT4/JlOOv0CtZ39CAj4/ajWMv6X+lv+GdSO/szVzf4yhwn/D1A6/22hUP+jTkr/cfJJ/9F0bv+NG4//xmeB/2vuWP80Ikf/pf9O/7fQTv9r5yn/bFT0/mVH1v6DI7n+yzWP/u4kZv5hlTj+VDL7/Rk+vv0Tgo791f5n/e8YRf1jQBn9QWfq/BoR0vy4zsv8A8S4/GS7qPwqb6r8X/6a/LhViPzF35389i3N/PCP2fxivs/83Nbu/PVoMf3lrGT9eMqD/YXZtf2Tfe79xbQi/l8nWf6ihIn+nQq1/iSD4v7AIAj/338c/41PNf+YAl3/g6Ny/4MHdf/BdIr/v4iy/7opyf9MD77/A2mv/yP0wv/h4dr/s9DK/7rDr/84jLL/c9zN/3q44P9ybN7/AJLh/7aM+/+hVBEA0P8UAOPnIgAsgVAAO5KBAORkkwDqc5oAVxLBAGKt/gBmvSoBNZtOAfSQkgFypOcB/JkvAu9LiwKp7wMDls11A7HK3gNbHVgEGHfYBNyFMwW80m0FykOvBSSi+wWZ/jYG+vFPBlI4TAY0qUIGXG1VBmvZeAbsT3gG3O9VBkKmLgYbGgwGJVXjBTV1ngXhKToFLZvFBCCcUgS9ntcDK/NLA277uwLXCiwCYOSYAQszCgGIhosAnBkaABPQq/8mED3/FavP/q7rav6S8BD+Iiu9/U+KZP1fmwP9maKx/PMogfx8wGH8DOE6/BIbGvystxr8Lv8z/H1MUPw0nXH8Fbex/GAMCv1BwUv9AnV6/U8q1/3UJ1X+f6iO/o3YjP4x677+fM0k/6AOUv/QgDz/+A09/7IObv+1XJH/bC2L/68kiP81n5v/H9qg/21sh/87OHT/a79u/3umVv96qRn/TcPM/kijmv7ayYf+i79X/jv1/v0rpb39Sd+q/VPHn/0nuHn9rZxH/TJUKf2ybBr9N1EB/aVj5vxm0Nf8aVfK/AFasvxsVaD8xyiq/O1ry/y/3uP8wKXv/BosEf1X7FP9eVWV/aocxf3sy/f9Nn8g/gfmQf7oCHP+WOar/u87zv7+ac3+8GnV/rATCv/0lUf/k/9g/wJNZf9k+3H/mCCO/4ofsP/MstH/BRnl/2ZT1f8ZYrr/BVLI/2wn8v+qE/L/tnzO/zg4xf/TSuz/2q8HACi0DAAqaCEAm4g8ANGuUgA9d1cAkXxVAHKEaQDtOokAKeKRAHGskwDTKrAA5EbqAFK9MAGsa3MBZBSwAaN1AAI4iXAC3hXpAigNTgN7S6gDfYYOBHnNgQSnS+cEu20uBQYVaAXdiZMFqkTFBXRDDwYmvTkGHoklBnY6FAbyKyYGo55ABismRAa8HxcGvnLOBTe4mQUiCHgFKLg4BQD8xwSMpD8EUZHDA2nOYQNO8/oC7iB5AugD8QGT84IBt6kjAVvOtAA7uT4AOkzV/0s9dv86bwb/kLGO/jaUMf7RfeX9rXGY/VYcTP0iYfr8kZvC/AzkvfyrH8D85Tup/JQso/w/4dX8sGkf/fbBP/3WtUH9neJ5/YhW7P2esCr+3HsW/gvXM/4dGa7+AWwH/4lh8v4/59X+i+8j/5gYiv9ZUJD/PTZm/1bma//b3Y7/dUqV//Bdj/8rA4z/klpi/yPFHP8bU/7+iPMe/3MFGv87NLL+XE9W/s4sUv6xalv+XAUj/oh42f2GPrH9GGuP/SoxXv37BDP9Pv8g/QiYCP2oze38GmjL/Pbif/wQeoH8obsU/e09Xf0skwH9rgvm/GRsO/3CfUX9U74K/XqlGv1xlk39Behg/T0Xl/21bgL+pM9V/sKXVf7wIDf+0yBt/hVXu/5S66v+qBWX/v2tv/6CJfr+YtYm/4GYUf/1Tqb/sJTh/8Ndvf/teIn/cbC0/6Tq8/8JAM7/GUCA/5SKYv+N2qH/6aIBAExcPQD6sF8AfsFkAEdlfwC4urUAAwKwAGRWiwCb5oMAyWSUAISSmQDRkLkAZeEoAT8SfQFG8pgBwX/GAYrYMwKXD7IClMXsAqSsCAPKFVEDHgmlA3fFvgMvP+EDrqlIBF1AjASLGJ4EGuaqBEgz3ARp4BsFWJE/BWe9SQU7zTQFt2YVBeeQ+QTqoicFqrguBY6LzgQbdl8E2PwVBL6P4AP1g4ADt5YjA+7H6wI646kC/IpTAgHACQI+Z9cBTBKOAUYpBwGYAZUAkAE8ABMd1f/9nob/iY1A/6JX7f41pbb+lR+k/kvHnf6Ag5P+p/x4/inIX/6Xg0T+ea86/juuRv5Ryzf+b2QL/hAL8P1UUx3+QAqJ/qut3/4Eev3++3IQ/0Z1R//k6ZP/S+W1/4w8mf/T2mT//3Ex/1jyC/+GYvz+1Trr/sMA2/4/asj+ZYO4/kTZxP5Q4er+DVUC/+0s6P53V67+boZ//u40Xv5T/kn+jRwr/k998P2rdrP9lcOR/XjNp/3sKcH9dsi1/RzNgv1nelz9y1xc/SCQTf1EUBn9nFgg/bEFQf2dz/L8f/Oi/FCe7PxOx3D9931G/R5Ytvx5dcz85/47/aQ7V/1/tj/9BDNo/ds9o/1ejZD9l6ax/dXLAv7NXOH949Nw/fIiZ/09R7n93Bjr/XAM3v1xS+j9vyT7/dWhBv45eVr+4u7I/rMhtP4Iah3+/LcH/sofgv6aBrL+G6yL/pKwm/710cT+d5Tz/p8yn/94TJ0A64/kACeFRgCQpgoABgjWAHaspwFb6ZMB4CoVAcDiBgGrqYkBscpJAvlR+QJsfEIDsd5IAyVWiANBKy0E5rHgBKTKNwWA614FXQumBQaEAwbOhFwGmQWpBteixQZqgqgG2smaBjzXtQYud7UGIudrBrWoBAZ9h7sFiPqEBUrBRwVaHe4E2eVxBPJu0gP+ID8DMkLpAncAngLgWxsCi2d2AWq8+gCAIb4AzaugAMgfbQDWthgAvQrF/6ZCmP/zgIr/MHd1/xWqRP/dRxP/JJb3/rPj7P6bZv3+RRYU/wZrC//S+fv+cz4I/yJSFf87qiH/OT80/9viM/8Kn//+lNDU/kqABP96Nkr/E/FB/2L/9P6AlPf+/dGc//2mQQC2AlEADRP7/2klw/8OEub/ocwLANfHwP/15BL/QJuN/nI+gv4E0bL+TIfA/ht/iv5u50r+uG5M/gbKgf7RDIP+eTgn/iqAsP2XjWj9rIxA/VxhBv1x09r8bAnE/E5Vt/ztkMT8exnc/L/f6vwW1OX8wD/k/BY2y/yZEo38zTVj/GyfTPzopk78yJhH/J9UMfzVzVL8rGip/B/z+fwB0Cz9FEVD/QFMOP0xhin99Aw0/dbDTf1hKEv9IaAh/b4xHv1IG3T9hY3j/SgGAv6BFQL+qTss/sEwOP5WqjH+B0Fc/qP5f/4reVb+61s9/lwvff57Crb+JTGs/j0Hy/7R6kX/E2Kb/61zmv89ib7/3wsxAOWoiQAgjpQA0a54AORyVgCqtDoAZOhsAGzW2wAQey0B/LlMAVDFhgFWUBwCbX6zAqn//wJPSTAD/IJ0A+PRpgP1tt0Db7BDBFSeqQRpfwgF4GmxBQCrkwYk0RoHdLAkB7xGDwdgHQ0HQTsWB/Nr8wZpG4UGXUvdBalZSwUrbiMF2CpPBSCbXAXXJuIEErkiBGLYnQPXrE8DG4XnAu/aNgKgM2EBF12tAPFDUACYCk8AoxVbACipPgCfZxQAovEFABTSAADpluf/xHWx/xs8ZP/SsRP/5w3c/k8y1P6bg/L+VXkd/0jYT/9r0JX/Ozm8/+vVk/9clXL/0AO1/7TT6f+waGX/dBmY/shnlf4x/lb/cPjS/1UPpv/rIHL/0aKx/yE3GQD5hkIAAXEVAPhWkf/YTe3+k+ab/omIxv48xOj+F/Og/tUqU/4gWXT+vva2/uD0vP73XIr+BSE1/tc+0v3TeYz9E81Z/Q2JKP0IBRr9o8sx/Q6TVv1AlmD9bFxw/RSwnv1XesT9y3SS/QX4Cf2AzZn8fXqD/ANNnvwOx6v8zKmO/Iapa/xkgoP8sNzr/Hu9Uf2NK1f9iwkA/Z0RqfzfTZn8dbS5/Koo1/xrPdT8HkPT/MkK+fybv1f9tvrE/Yt6C/7bARf+ACjz/dw9uP2iEnv9chdy/Zckwf0yZAH+8KHr/cfG9/1a0HP+Nur1/hNJJ/81dxr/0vPg/j/QuP7jL+X+AdBF/wB5hP9OS63/ZQkWAIVCwwBUq10BBqOoAVAQzgGHcvIB5OrzAUAF4wEJk+MBG0jmAWlPDQKoInYCMYbwAmdZNQO1oWwDiKbrA88LbAQWN4UEeGBjBFrifwRWGNAEkVUcBbKZegXN/M4FJWz/Bek4MAYvhlMGKF9IBnJmIQaSm/wFcQPGBUEJZgVa2+EEWfxxBA9eUwTMCEcEc371A7UIZgPqrdUCut5nAp9vKQJUtN4B9zFRAUFJtwBkFVkA5WRJAHmxTQDIuzcA8XQRAL1f9P+5jOH/i6XM/4+wt/+zNJn/8jJy/9NBT/8FXTb/J7JE/0PEeP+vvpj/pHaP//zyhP+lQZb/UZCj/0upjf+fn2v/I05p/3xNc/9MYFz/yD46/40MVv+OE6j/c+/R/9OHvv/aiJv/MfqQ/wKDef+BnUD/ASML/wx9xP6ecXz+p2Vu/smaav47NTf+klX4/Zw84v1y2OH9l5q1/ZvRWP2/eyD9HZgh/adsDf2Z6vj8ZVkQ/aWqK/1TSSn96Z4z/UbNR/16uzL9jLEU/f8vDP0VrPz8yp7Q/MZdn/zJy638q8jm/KlpAP15BhP9z+Mj/R8/If21Ah/9bRo2/d24M/375wP9Zi3q/NKt/Py0Kif9HQhV/eOfkf3hO9f9YPT8/ZFrDv4mmBf+YKz9/R/+7v3dIxv+BbE1/gLMAv7r6+j9CcUq/uEegv4zXYn+KS1E/nHUX/72AQP/rX9H/5XQDv8M9xf/wT5g/1Xvo/89c/H/oVYzAIRZOQCmODoAk8JnAM9PyABUGDQBZI1nAXLdoAG45/cBveQAAnks8wFXQVkCQbzOAnAB3QJkINsCoGUkA+PLmwMmTRcEn/h9BJoE0QTK3QoFuFA9BXLOpgXjrx0GROxDBmg4Owb7XDoGIVIZBuGx8wWaNPEFVpDrBU8eyQWjfm8FtNnwBFNenwRTaH4EVw88BIVPywM5OygDFQFiAoCw7AHyftEB1WCQAecrFgFjjKgAKiJoADwaTwDpHysAKljt//c5t//CdY//hCVg/y0hQf96Gzf/ssYk/2WwFf9QtRT/qJkm/01FWv/DHWz//tIz/7VyC/9CBAz/Qvof/yNLLf+/+h7/XCIa/xunOf9xxFn/aXJi/3mKdP9WG47/B9GR/5ImmP86lo3/9qdV/+DsJP97Oiv/3IFR/8r2Mf913b/+tQ1r/pHza/5ARHv+1HVc/vLtFf5rTcr91mqi/fYxh/3CFGL9iMpR/S9OXP0mLlH9zi4l/erZDv0lviT9av49/aD0Uv1K82z9e4Rk/aeJI/2Lw/P8pN0J/eh6Ff3ePvb8GmTi/GIo5/zWxfP8BsYZ/eS2P/0/9Tr9Bucp/cnqHP1l0B393r4m/RjBIP0ulh39qSFA/X13ev3p4Kf9Lazf/VG6B/6jwvb9jGgO/oXqi/41AvX+I8Of/jR9Hf7BbZH+ktRx/3Nzbf/Lv7z+Jvil/p3dFv9tCED/WbVt/0b6w/8hcpz/WFUi/zQoZ/9FhGQAzb/PALYkbQC8P0AAM9+YAKRe2wDRPwABZTJSAaJohwEynWoB1VViAVShtAERmv4BujgxAvEEngKnD9wCUq+NAvbYXAJbNeACheSDA0YGogOnWYYDFNiUAz4n/wN0dqEEN0IbBeEqQAVmiS0Fym4pBUQVZQUCW6YFtZR5BQPTEQWNxPQE6PgOBaWdFQX5vfcEYuGlBDM+QQTNEPoDl5TBA4w0VgMutrgCgRUrAnTG0gHef5kBOQlNATOo/gDseccAOyCVAO76VABZSBMAFRbU/5WLoP8asn//zTZo/+btUv+vYzH/9tcq/w0cS/+G7VL//T00/1xpIf9MoTD/CN1D/9VOQf80oCr/byMG/73jDv+pI0j/ho5v/xnIZ/8AJDf//a0X/5zpL/+dbHX/LJTC/6okyf+KjnH/JMAj/0NdMP8tfXH/x21m/45E8f7OZ4L+0nNq/gm6i/7qjJn+i019/rbkQP6JoPv9RKDZ/SP8yP0syZz9LYt2/XTMYv3W50z9WpY6/TfsQ/357mz9exGE/eeJdv26IWH9aZ9V/bbbNv1UgAz9sSgb/eN3Kv3psf782Ef7/MPDPf1GTWL9QIhS/ZIZVv391n39s2+O/R59eP2yFGL9E1Ns/ZRrif1K5ar9Lsnn/YrfHv7ASiX+Yss8/qZRdv4pcYb+zIZr/oTsaf7QKnT+V11t/mzPj/4pDdv+r2UI/1fwAf/40wf/bt0u/ydkUP+ryUb/ABhD/4AOWP9Imlv/g1t7/6Qe4v/USjYAwZY6AADPWwCjz7oA/RnvAMJ+3QCcBOcAmU4MAYQAGAFILRwBMYU6AdDzbgH6NZcBEDKvAbOc6AGxxS8Ch/BDAqltMQI/sTQCFJdsAtPUrwJ78v8CjBlGAxZJbAOVpsIDZLdOBAFVqASgMKUEp0SQBLVyrwSyO+QEEpv3BM2H0QSW/5MERal/BFKUpQTl9cgE8MeYBGNFGwRgjaQDViVxA+WWRwP8leACntpYAvJe7QEynrABH5eKAZYRXQFWfxMBQb7NAJC9pwB3PW4ALqcXAFZq1v/W4cD/4Tax/9lQjf8nGmr/g8xZ/5Cqbf/Xtoj/wF2B/3nWVf+PBij/ydcd/1cZOf/VREn/x6sq/2DdIv87VmD/Nch7/7QrTP92vzX/V99g/5ETiP+noYP/JN9h/6LjM/8ahkD/J9Wd/+QTBAD+9gwAJYOU/0aHCP8i7+b+rQMQ/z5qBf/ptLP+rqhd/mpfJv4z8TD++0lr/vx4eP7NNzD+I2fK/WM1fP1Vvz/90LMb/YEbEf0fnS79nzVc/ZSxYP3c+Fz9I0iL/VR/xP3ChM39V3el/QVLV/26RBH97lgf/WJ4eP3ggbb9JU24/RmPuP1Bw939+sQY/iz7N/4DBRX+jQHc/eFDuv1tt679OBjB/RnH/v1KT1P+2M2Y/l8JxP6tSNj+1Nre/lVJ4/4zyNn+ECLP/lvixf7q05j+EDKX/lQcCP8/Cob/E/CW/4+/b/+5TXn/OIKn/7gls/9Gr4D/GddW/zoWX/8LN3T/dKSc/xnK3v98thMA8ZhBABKZigBBargA6qmaAKCWeACHpY4AV+HSAMj3HgHkd0MBUHpFAYMvWAE3gIUBUjC1AbFYxgGyT9YBmcILArtbRgKAVEECwnAeAqy1QAIn17YCHykxAysJNgM+UesCr6/4AnUNdgNdVuEDr7DyA0r2zAMOxscDu78PBD8zXATtkkgEt0TsA1DtpQNEUqEDf0rBA/haqgMoS0QDaq7uAqH63AK9jNMCJFaYAqTBLQK1IsgB0t2KASRqVQE0DQwB+hPJACAxowCWkZMAo6t9ABjBSQDI5REACMv3/xUM7v9cMtb/w2+w/0IYjv/XF4D/9guH/4V9kf+oxpD/gV6M/+DvmP92VqP/Z5yI/yUDYv/8iVj/leRn/2nqcf9jv3f/gPR2/yOyT/+LtBr/56Em/+WKhf92s7//Y/tk/7HWx/4qSJ/+58AH/9Fwe/9jJ2H/YrfC/gnYS/4LEVT+Gh2O/tJRm/6YM2D+ulsL/pmY3f3GqM79gmvI/Wn6zf22LNr94XnK/VIEpf3mPnb9oY9P/WHDbv3wprz9Uwvm/Ztg1P0Z9Z79nUKa/Tn5+v0DvEj+7jAp/n4t3P1vrLT9lvTR/WnpGf5asFb+hIxZ/uqlSv4l5lX+9Q1x/lhCk/4WRJX+TyiK/p4Bi/4JyXf+67Vn/iHnkv6Pq97+zQoe/w12NP9W8Qf/1nrJ/vcgyf7h1/7+q98m/8GfHP94T+T+YpjB/vWG7f5i7zz/ij5r/9jLY//GyCr/psf+/lM3Ff8J3E3/hxV3/21Eg//VRYv/6j2q/86eyv/rZ+X/DiwbAIYsVwBxGE8A+uUQAA4q+f8EFhYAzqdtANs94QAftxABLNUAAZQsBQGb+C4BzfRcAVdwawGtjlABMtU2AUavUgHaFpgBd6fmAR3ROAIeHYsClbLdAoFoDwOQrRsDj60oA42jRgPaoHoDBYutA0CqvwNfmdADNIbtA+Yd/ANPQ/8DqqL9A/ka8APyodID9UG0A+OQgAO2TkED5zciA1+xCwMJbOoC3re6AjwDaAKa1xQCAU/pASI6wAF54YMBJ1g+AYXR/AABDtIAnW+6AKaRnQC+5HcAzS1QAFljHwDXafP/kLbW/2yRvv+877T/szO1/3YKq/8qjp//jkSX/8wYjP+mZYT/NyN1/zuWW//NOj//16QP/1gFC/9x8VD/Xl95/zB3RP+0UA7/M/Ak/66VSv+tpzP/0Snp/s9Bzv6BvAP/6GAX//Fr1/6aL7H++HzK/nDN3/5bQLz+ythb/gWMF/4yhTn+JSd6/otFd/7EVjj+qfLj/WvFw/2jGvz9PHox/m1PBv4dr6T9ZXF7/cuttv020Ab+KNwB/lsb3f3aifb9CQs4/mHpS/6nqir+iugm/pgfZf5WJpb+IMV2/ksKQP77DkL+nBmC/ooD4P7Q+Q3/yGvX/o+Jmf4Hdrr+0rMP/7aIKf+ZDu7+lavO/tRsAP/yuTT/ZsUx/3cYGv99TTP/ys9s/8+Mi/+dfl3/dMoM/zer/v6jrVH/872S/70Yav+pWS//TKVm/+kztv8aA4//j4lO/xMnPv+1az//Ikg0/wOSN/964kX/Jwhd/8Cldv/4U4//JxCm/01El/9wk4T/lhi1//Sg3v9o76b/ie+c/wkCDgCm7ZcAjobbAE13yAA+/4MAIemDAOh/8QCBr0wB49Q/ASfBAQFTw/8AXL1qAStN8gGYYBcCQh76AQpe7wHB3BUCx/9mAjWZrgKpa7wCMufIAsYODgM3mlcD0ApvAxOhjANtLdYDLMYSBIxUDAQjwrsD72B3A+xiiAP4AdID6kj5A0HsyQMlvWsDJSosAxBJJAMp+x8DYjjeAiI1bAJ3AAUCgXi9AS8+kwHwDIMBLPZ1AXLOYAE98TYBKgTiAPYQfgACBU4AUXpOAAJpPwD6TAkAG7y4/9lWjP8HbZ3/uLu9/264uP/3NY//13NY/+1jMP/5HCH/1SES/6YP+/50BvH+k1/4/kLW9P7k5Nz+sSTM/tCC4v4oCPz+/7D0/sA70v4SBMP+mJ3G/maPx/4/2Mz+HgDg/n9j+P5Ko/n+i0fk/vOg4P4XHf/+6HgY/2X1Bv+PYLH+XsRQ/rcyQf6Lb3X+7FWP/nzpZf4RRyH+2xAD/iA0Ff7dOSr+h7Qn/pU8D/6U1ef9wcbO/dK9zP1cxtT9btDx/WhhQ/4DUZL+jVCI/q8GVf7eeV7+Hcuu/tf66/6ERM7+y65w/qQdQv5J2Xj+xezd/m73F/++ggn/G9zT/pzWtf5oR8P+8krW/o90yP4TyaH+EXeH/imhiP7IPpn+a5yq/rBGx/6dKej+kbvt/rtuyP7BeJz+sW6V/lBS4f5dmkr/DHQ7/3rQuP5NeW3+U8C5/soLQ/8pNXn/dzdP/zUgKv8nAUn/qzOI/89WxP+e1eD/T3+q//K1f/+gC6v/km/Y/2+a7f+gqiIA+pxyAIMWowDXm48AOW1ZAGsLXADUGrEAJtICAaNKBAEfL8kAE6uaAKLo1gCXlloBOPuZAbU5dwE9/DIB/MsRAZLTSAFpNKUB7xa/AQzGsAFQc8EBsw/4AamNOgI6Q2wCSnKSAuH/uALVIsECKKShAo9WmwKE+d8C25FLA3ZnqgMlfMUD++ucA1eSdAMN/GsDa7d7A9NTfAP6L1MDFXoVA7409gLljvoCZFf+Ag1N7QKyxMMCpH9/AspkJAI6L70B8qZtAeYvTwEFGT0Bzx4TAWwE1gDmy5wAQi9vACweWADOmUYAYzMbAB85x//hAXD/0a9K/ypdWP9OXXH/qc5y/0udXf+JUT7/chUl//b0JP/w5zL/U2Eq/3lyAf893tv+e9/m/tH1HP/AvVX/IOxr/7f6ev/Pf5L/TU13//80KP8A0fv+jOYp/9ZYdP9dTXv/jzY2/16ZBP/Cwz//VByn/4rAtP+xMTD/oSSA/i3iQf4Pwoj+sbbI/n9jsf6+fn/+yK9j/lk3Yf7LX27+OkJ0/ut0VP7C/CP+1xX4/Ru26/2Yev/9Cd4e/sQsTP5mw4H+RfCc/jK7f/7EIUz+UOw8/nquaP7EI5j+zbuH/rsvVf6njkj+60lr/uyJpf62odP+8OnS/gxqsP5dL4L+8IVj/maVdP4Gyo7+tlWY/nHxm/4HGJz+3cCQ/nV+k/7kkbv+Tvrh/kc56f6oA8f+g2OW/sPFjv4QKrv+D0r0/g93FP9NXxT/ZwcW/1a+N/9k2m3//BuU/7/pmv84R5H/+4aM/+aRpP9So83/RFvn/1wb8v+1kfz/ZMYLAF8aHAA/AxwAln4EALSW9/9b2QQAPc4QAN88AwDZ2AAAbUwpAJzbaACOsY4Ae7BuANqcQwA3jEkA6/lgAEFkeAD9PqAARYe3AHjmwwB0BOwAIRcgAc8jPwF7RVkB3VWCAdT5qwH+DN8BEWUNAoAaQgKS+ZYCQ1znAtzkFQOCezQDrl1WA/H4iwNwwMcD9VrSA6ArqgMPnZYDvme3A9z77APdARAEmmb2A+JargPfXXUDgaxgA8BcRQNaggIDA0isAu3VUwIy5wQCK+fKAZMOrAFDPZEBlZFeAbY0CAE1YaUAgK1aAI00JQBsiQgAgFPy/2xBxv8aN5D/8bBx/5Y4av+1XW7/+VZ6/7TEc/8zfkX/IGoY/5krFv/JsSr/Sn9L/5b7Yf90mU//nToa/9fBA/+CTUP/RhSa/3gon/9v80v/62cM/4uuH/9Kc1z/2E2C/0ODff8UX2v/PJZu/3k7d///cln/SgIr/3S6IP9bUjP/Kywc//7gzf6kQZX+oJCr/l6a7v7k9Az/NuTL/hOQWf4SPR/+BZA2/vswY/4Skmf+PH9C/mdUI/78ECv+EcJD/jWYTv5NQkr+ZVlC/rMFP/4Qe0L+dgYy/sRcH/75MEj+y0mh/qAdyf7s+Jj+vB9P/s4sRv6FLpH+z5vM/pmLuv5/qIL+v81r/rabkf6a7Mr+Db3d/orfzf5f8MD+2CXA/ii+s/4FJ6T+Td6o/rjTzP5M4P7+dysH/5bR8/4SEgD/ujhA//tJev9QDHb/VDhI/18oLf8h+En/fiGN/8Dxwv9p397/VEvj/9RM4P+aI/v//7MRAMJuBwCCZ/L/Q534/8PNBQBEavr/N4To/wEn+P9IHS0A/nxRAKU9MwBUxtb/XRee/1u1v//Yl///Lz0KAMlQ5v/M49D/dLPx/8FyOAD242MAzLNbAPNOQwDUUkEAufhEAMM1OwA58zAAOXRhAL1+0ABS7R0BKbodAY1TEgHzLDoBPIqTATF0+AH5BhECDqfkARmr6QE1YUMCoX6vAsy59AKT/gwDeQEmA/g7awPaO7AD/BW8A6YppQMNL5kDx36jA6EJrwNFsJUDBe9mA/MyZAOwSn0D18psA3EKKQNz4tcCC96SAr/4agJv7T4CalroAWVQkgFM1mcBKjdfATkpRQFBaQwB5uLJAKMAlgBTxXgAQSxSAFGBFABaS9X/Btav/xH5pf9HnqT/+UqQ/4pCe/8TQnn/1tVs/47EUf9JbDf/XFQj/4q+E/9I5h7/SwQr//PiFP+RSwb/3Zwp/1pVWP/wDVL/YUs8/7S2R/8B0F//mzY9/7eu7v6IjfL+2q9Z/0Z4qf8Ydpj/Mo9g/2/YU/8sCmz/28lk/2HvIP/uXNX+Ja+g/m5Oef4YbXv+Bt2q/pdcwv4iXZf+/iNl/iY3Vf47/jz+UTL5/Vb7tP1ptp/9iEmy/a0Hwv2U4ND9POfk/QHD+v2LcSD+qwI0/i+lGf4t5un9PFrf/fXf+v1gwxb+L40f/jdbKv7M207+cp54/vUzl/5WlqX+v0el/modov4p3qT+m2in/l3Krf5/mr3+i8nW/lj1AP+81DT/ChNQ/2nCS/9idEv/t59n/6/ehf9cY4D/DxVx//1oeP/N0Xv/IYBy/4szgP9A+pj/v12a/7dujf+634j//O6J/yljgf8/an3/7hSQ/zRQpv8k96n/0h+m/96Tm/88ror/HASS/8Lqt/94Utn/6qzU/94uqP9ASnv/3gSI/x9wxP82afT/ARIEALkF9P8YwNX/menh/92fEQCNQj0AZw9jAA8pdADCGXYAyB+IAADApABEQbMAr9bZAOx2FQFztzMBb9A4AfVTPgElaWMBC2u9AZ7iJgJWe2EChYZuAuI2cAKqaYgC8efEAkHvAQMYIB8D0msyA+XPVAOHjn0Dd3mmA1qUyQObWNEDy+S+A3tpqgNx0ZsDuAeJA/wXbQPXV04DywA3A8ZCIgN2/v8CbEnPAvgdmgKb/mMCJrcpAvKV5gEcIZsBuxdRAV3xEQHJrN8ACq2sALq9ewDen08AefMmAMkqCwD/ifH/lofG/63gjP/NF2L/hQBM/yy5Q//CFkH/3wgt/6VJGv9Wmh//RgUx/2AAPP/Jxjb/0h8x/9lZO//+4Ej/sb1E/8+lP/9GO0j/qPJY/9Ihcv8Baoz/wmac/5Wpo/+mjqP/xT6d/3xtjP8RXmr/M0RD//WgLv/CgSf/XNUL/zgL4v7118b+A7nD/mlWwf7S4KL+latz/iJPRf6uLhP+Ribl/XXH0/3Q2Nb9Y4ba/dOA4f3ZwOT9jD3o/aj49P2Edw7+AYso/uMvKP71Dgr+YM3u/ZHI7v0fPwL+f9Yl/qnBQ/4n/Uj+9tZE/lCnUf6OL2f+MFt3/ueZg/42UYf+bId//ugqdv7xwHz+cUKS/ggis/69Etr+3KP6/thY//6vuOn+Tjnf/tEg8/5PLBv/MCtB/6HTTv8Atz//DgYx/wcXPf85Cl3/UCJs/97+aP/T12n/4D1s//9LZv+TVGn/hN6U/7Voxf/93sH/mqaZ/2OQh//+C6z/BerY/4pRz//4Z6X/K2GF/2Lphv/phKH/8s3M/4rO4f+iX8H/kJSg/5RytP/A+Or/V1wHAGLZBAD2qRwAuFRLANFeVgDnOkEAwRFFAMaeiAAhTNwAQKv/AIlN6ADr39oAKYoUAS3UawHKaZ4B2UCZAfFChwFqCaUBmSLvARpHOQLn4GMCgV98AnXIoQL3EtICze8BAySnHQOPmioD2L87A+AAWgP26IcDkzKsA6szrQMRxJ4DyWCmAyphsAMP/p8DqMOGAyW0dAMhxlkDIO4oA08D8QLZasQCywejAk7veAIo5EECOEcDAvbRxgHIY48BhR5fAf99MQFMnfoAhxTCANxViQAAdFoAQCg4ABkTGgDlYP3/B0bg/5EowP8uFpb/Aclr/7nMVf/sKFL/6iVP/2keO/8gCBf/hUQB/0QuBP8muRL/7MoV/1zTAf+K5O/+fEnw/gC5+f5qoAb/ER8V/zGYGP/EPxD/edgl/2FdZ/9WzI//X5lx/zf3P/+/PD7/VgZV/3HjPf9jFf7+sVfY/ud63f6X/uP+cijQ/nZ6s/6Bx6X+JqqW/gsIc/7ea1L+gdQ5/hVaEv5Dve39zF/q/R0S/v179AP+qVb1/VR07P3QKf79xJoX/oBcGv4lJAb+H6bm/c0G3f39h+398uP4/bwD+P1aoQL+F2kl/hbqPv5Ld0T+YCNM/srNXP6wpnL+CKuH/nSblv72JJn+lOWJ/jlUiP4xarP+gu31/mtjHP9ZDR7/7h8h/1PoOf+qN1b/zRNj/80WbP9oa4P/sXmc/0XSo/+nEqH/u7Kh/71mrf/fU77/+jzO/z8syv86Sqf/xyiK/wTrof8afdX/DkDz/ziF7f+HNOb/1lTq/yVb9f91tQYAT/0DAEYb6v8AntT/a3bW//PL1f9G0sf/k5zI/zrc8P9cuyQASYA0ADvfGABEhwAAIG4TAObmSwAQpHYACut0AKZXYwDSUW8AwaidALo21QAlzQIBDOMhAQCTQQHbnmIBYwt1AfmFdAEsRYgBpBG/Ac3OAwJL3DECca0/AvlRSQI7LWUCA4acAm242QJGo/UCglDjAmAyygL2ENcCJCf7AigdFAPexx4DJ20nA0U+PAPe50cDB+E4A0a4HAOAkAUDG1b6Ahup3gL7Tp8CJtBSAgTsGwIyPgcCGKD8AQur6AEV98ABFF2HAZ6BWgEnRToBUnITAU1F2wDl4ZkAl0RgAHp9MwDA0A8ANMfz/9CA4f984+D/vaHm/5SH3P/BGMD/Ntqj/27Umv89yYz/Afxu/4ZYRv9kcB//oBII/wCZBf+QrRP/8ykf/0gdG/9KvQz/GC8I/1rdHP+NwEH/Ntla/6NwTv/R0i//twAh/0oHIv/N9x//wMIO/4W4+v6gldr+Pr26/jTvr/4OSbz+XNbL/v0Ovf553ZX+VT5w/lRxW/66PEf+ykwm/skTCv6hrff9gPrm/Tk6zf0UObr9zXLI/Xi36/2nYAD+md/y/Wst2/1eAtP9P4nk/TRX+P0STPf9ERXq/dbV3v022OH9FCnw/egJCv60ZCv+afU//oUaS/7ou1f+ZTlr/jdKiP48cqX+/3W9/shiyP4/jb7+hpW7/vUP3P5ttAz/iZY0/91iUv/F9mH/10Rq//YriP+Vebj//GzL/2xtuP99BJv/bWGE/xNpeP+bTnb/+FiI/3JorP+sVMX/iVvH/1znyf/cd9v/QRby/4ZM7v9Lv97/hFHT/x4r3/8CI9//6HTL/1L+4P/qjRUAXL9BAAhkSwBCl0QAcBJCAPgqUgADeGAAb7JaAJLWQgB5ND0A6xtUAORUegBn6ZcACXKqABdexwDqGeEAcNX1ADmA/wDYZwEBKRgEAY4aEwEjMSgBg1svASD0LQGdlDgBC7JUAUFohQG6cKwBHFzDAU6+4QH1FBUC5RNRAsmPbgJME3ACuwF9AgsfoAJT07YCFoG3An5CuwI7ptYCfrUJA7s4QwOw7mMDKd5sAwesbQMyrXUDSLN3A3xnXQM58CoDtS/sAnriuwK9tpsCWMV9AoEKUgL3sB4CTZ0AAsRe7gE0EscBC1+LAZJrTwErViEBZ/3tAGKKrgCdg2gADrAjANEE9/840NP/rD2w/1RBi/8Zlm3/fvJg//0/V/+0hE//dlFC/4tMLP9NxhT/b3EF/5lS/f5q1ev+YOzW/lU/yf4K1cf+flnI/nTY0/7AkvD+ffgB/2zpA/+CLQj/6xkQ/64LEP/zrQD/tkzw/uvh5P6pBd3+KnLR/mxatv4ZZaD+HZiV/jprmf42JpL+3Hh1/lwAVv4iYj3+RVcp/uSKCv4YPOj9i07F/aFzsf3X9ab9eiyi/d1Inv0vPpv959+e/VmPof1p9qz9geu4/SWCvP2u77/92cjP/XmE4f3/Yuj9mADo/YdH8v2Kgwj+4FIn/qSiQP7tNU7+wC1o/kYLjf7RPLT+MJbN/sTi3v7iLe7+9C34/q7EDP9XWS3/UF5I/0PqTP9U/1D/Bz1s/78yjv/1aqX/XBOu/90ks//Z58L/hHrX/wuO3v/HpdP/YTvQ//Cv4v8Y+vL/L43x/8JE5///jen/YUH0//fB9/9QTP7/GtIBAAgsAgBtCQAAJOEIAAlGGgCJqB4A+fooALBtPgDMXFsAmXFzAFnyfQDRMIMAI32UAAxlqwAiybIA8+elANuGnwDGTbQAiR/YAM08+wDnwxUBLpc1AYDFVQH6QHgBymWVAXG/oQFEC6UBi7isAVZNugGBdccBcy3PAXSd2gGNGO4BqHEEAiftGwKruB8C1y4YAg+yEQLj9BcCqv4bAv9TDQKPPfsBtnbxAdFk9gErBP8BLdkFAslcCwK+ABICWtYlAokAOALArDUCM04qAt+xGgI4uBACXpwCAiy/8AFOrdgBEnK6AcfIqgFfwaYB8+SiAS7wmQFXDpAB66yEAU8dewEvbGgBbuxUAc8KRgEQij8B7Qw2AeBjEAGvKdkAcjekAA1xeQAdvlEAm0oqADqqEABjBwgA5nv0/31S2v+VCsj/RU68/3jlpf9X+Yb/3hhu/+7aTv+WeC7/gs4Q/9LN+/4YHO7+12/f/naa1/7cJdT+imPP/hzXyv7rnsb+Vl3D/kOntv4/yZ/+qZt//vTXWv7cqEL+USMn/sln//20mNz9MinF/a2Htf0Gnqj91eqa/TkrjP2Qv3f9nnBt/RkVaf3OqV/9e4VZ/ccyUf3oYFH9xEhU/QLCUP30W1j9kv5r/fg/if3zvaX92zK6/VVLyf1PZtj94Ibw/SjMAv7TPQv+G/QW/qmfLv7LBkr+Nn1i/uKad/4wE5L+eJ2t/jwqwP5x4cz+xUzZ/rkN5/7+nvL+70P+/pfQDP9n6yL/dfs//y44YP+8SYH/xLWg/381tP+vpc//VmoFADGcPgCOtWAAc3RvAF/hhgBrcqcAwm++ADklxAC/A7kASY+tAEPDsgCtw8cAu7zQAEJzxAB2KcMAKZ3gAEMmDgF/PS4BoTo1AYpSNAFiyUQBmRJiAYeBagFN9VcBvppNAXOGVAHCimQBXK5vAUavawFpU2AB6b1dAcO0cgEdwIABinp0Aa+OZQFHkWEBPXlkATAYXwGTjEUB+WccAd3yBgFYvBsBK+gvAZpsIwEoPA4BB64WATpKNwEftVcBt45sAY9KbwHjgG0BspZ9AYcLlwEHbpkBWn+GAZBgfQFVHIUBdlGGAYsnfwEcd3MBjSxlAWdmXwGM9GUBRDFoAR05WQHkuUsBXANOAegGUwF0mE4BI5Q/AZeMLgFuBygBmGMsAQOoKgGhjBQBmPj4ANKy7QB2Ee8AGErqAHF94ACBi9cArB/PACNOxQAoZroAil+mAO76igAQ+HgAn7VrAFYrWAC9ID8AmUkoAGElGAC8GgsAqegAAEmK+P+Uwev/d4vf/0qL3P+8fdf/Z03J/xoMt//BOaT/K6SU/5uKf/9Vm2j/aD1P/1U4Mv9ZASD/VrQT/4MqAf+a7e7+pRnk/q7+2f5xuMf+bAiy/gdnof4gjpH+lMmI/verhP5oyHr+MBpn/iVpV/6hLFT+7DxM/rW1Qf4bDjv+P2Iy/mUkJ/5bYiH+vlUZ/jdbB/5q9fr92xwA/k96A/4fKgD+J4YF/puvEP7Y3Bz+nG8t/l6gRv52KF3+5ets/mjLeP6wZn7+1td+/q9rhf5+jZD+JNOV/jhbnv56Ia7+0SzC/r7h2v5R0/f+2kMS/84aJv/G8Tz/ZOZZ/wvWcP8d7X7/Wm+V/y9er/+SlcD/LUDQ/+5k5v98agAAPyYWAKDUKAB1mjcALs1BANK7SwCtJVgAQYpfAHzlXQAM3lcAcpxQAKryQQACnjwAFa1JAO+TTQAGyUQAbEtKADtyXgAsR28AhhGGAIlmnwCoAr4A1fvgALMU/gCGRhABLUEVAVn7GQFBnRoBBFYOAa3c/gBt3vcAUS70ANKX6gAIHOAAdp/cAJON1wDE/88AYBzLAIUKvgCVgqgA+kyWABgbfgBI3GUArklaABRcTwDTA0YAMNtIAFmuUgAb4FMAzUZQAE/0WABs7WgA7c92ACwHegCz43IAu69yAH8WdgCW8XwAIcuFAFkLhgBo34kAq0CaAOUGqADYkLQAaSXDAON/xABo974AhF28AGfgtACtX6kAATubAEevjABpYX4AZEVxAFkdZQDEZ14Aoa9mAL8ZdABdFH8A4jd2AAM9aACUuG4AqE9zAP6HZwDQolUAebxAAPb6MQCJ6C4AuTQsAP/YJwBwkCgAMJAxAP8eQACg1FIAh3leAIYKZwCpx3AA0sF5AHAAggCKXn4At8NxADe8ZwAZi2MAl3ZiAM9iXwBR4lkArZtdAGinZgBxSHAArrV7AJPJgAD6mIYAsc+VAP1HngCHP50AJ4OfALOroACfs5UAV7eDAGuNcgBMKFkAeNg8AGeXHQDJkv7/A8zn/z4B1/8mrcb/50+4/7oCsf+aIKz/cKuj/yJ6kv9erob/fMh8/3u8bv8v62D/ctpU/1x7Uv88iVX/aqxY//zJV/9fO1H//ppK/zgvRv+zPUP/N+I4/zJBJ//vlxv/o68S/7XmD/8YsBP/pTgT/0piCv9FqAL/7QoB/5Kg+v7YvPf+wjL7/tL//f64qwD/1e4E/7HgBv8gnwL/x3wD/1p/Df9Koxf//V0c/5zbIP+7kS3/zq44/2AyO/8y4D3/DlQ//wAURP/2BEv/VWVM/0CsRf95wz3/sSpB/4Y/Rv/gPU7/M4VZ/5jyXf+Y6l//uNFt/1W0gv96Z5D/f0CY/4IXpv+eZrj/bYXM/9cb4P+9n+X/L3nm/+BZ5f+ZBeL/Rxff//lt4/+mzOr/ldbu//297/9fPvP/D5kAALiRCABtYQsArycKACjKAACvh/b/g3T3/1AtAADRMQcArFQQAK6hFgAAzRsAoQAmACvUMgA5HTkA4kA1AF5xMQDGBzMAauw4ANe9PgBcWz8A6+M6AJ1LOgCgUD8AFJZEAP5LTQAkVVsAB7VpAFEeeQDq7IQA1WKQADm2nADU1KsAxUa7AJ4wvgBj+LsAwhe7AIMZuQAoFbUAQh+2AD4nvADC4sUAX7rRAK9U2QDFatcAeF7XAPo01ADwgMwAAzDKAE4twQA62rkAQVmwAJGcpQBPx6cAj9uuAH65rQB4+qMA/4ecAH7UmQBMbZoA/bCUAEQyhABxp3wAXvB+ADUjgQD0zHwAO31xAIzDaABfW10A8fdXAIZEWAA/p1sAfa5jAPEVZQCH720An/R/AIhHjAB/TokAMWCGAF5IjACFsJMAWtecANNmmQAjIpMAFFqSAHFAjADtLIAAYwJzAE+wagAauWYAGFNbAP3uTgAkN0QAtbNCADSnQAAglTcAUeU0AHRnKgDMxiMAxn8YAEmaDwAMgAoA2wcDAADvAwDqkgIAw7YGAEhvCQC6yAIALRvx/2IY5f+OHdn/b47E/0pErv+ulo7/frt9/3tlev9ICnb/clhs/1ReYv9T+mP/GZtu/ywdd//VUHz//LWE/4dLiP8Y0oX/oyaH/0EQkv+f+KH/9pOr///Fo/8kPqL/cS+n/xeHpP8jx5r/K4qO/01Riv//roP/AHt7/1BKcv+u8HX/C+p7/wbTdf/6w3L/Qity/yWGd/8usHv/i2B1/xY2cP98P2//8B5v/18+dP/J437/tQqL/4EOkv8oYpH/xbCT/4Pqm/+aRaP/Jfef/8Jhk/+eb4H/fDly/5IYbP+vv1z/XC1S/zJ+Tv+IVFP/DwVe/xZjY/8I+mj/jLpt//jxev9VU4j/3TGQ/3YVl/8/mJ7/cRen/9j3rP/z/7L/Aeu7/6DJvv+RncP/hq/D/1JWzP9KkdT/Eg7T/y4u1v/qj9L/ozfZ/0h45P/XPPH/wv73/788/P+W9wUAuWwLAEwxDwBz9RIAIMITAALQFQAPqRMAIA4RAJq0GACv+BoAG6AUAJo9DQCj3wkAYQwJAHC4BQDC9vz/9dX4/4Pn9P/BVfr/1EX+/6UVAwB2tgwAFkMMAA4pEQAL1hYA+8IYAJbWCwBL2wYASYEHAEubAQA98fr/ga71//k0//8jlQ0A8aIRAD1zDwCLVhgAPRorAJKjOwAWiUIA7bJNAOvhYQAzKWsAjhhqANoAbQB3I3AAVeF3ADIodwCpMnYAxoCCABkNhQAza4cAqXp/APt3gwDo2osADid5ADZ8ZgD8R1sAClFoAAIocgC/I3EANe9vAIv8cwAocHwAXlJ+AE64egBPB3kAJvZ3AFz6ZwA5zmIA8HdlANPPbgCQ62gAoGFZAD6ZSgCzBT0A+046ABGrNQAHhzMAEX4rALS7KgCu9h4ALfcfAFQQLACl/jQAms8/AO/1OwBXsj0AmP8+AMQ1QAAMG0sAVk1TAPEcVAATCF8APmlfAJjjXwCTh18AD+5fAFV1WwAgC1UAqlRIAMEBQQAPoTgA7TYwAMMhNgD60C8Ajx08AAUvMwBF7zwABqVDAAtlPwApvjwAYXoyADkzLgCYwx4ALFkdAEssEgByRwgABsj5/8PY7P9tXef/4GTl/2ss6v8kQOL/31nc/7k71v8XEdP/DezJ/2H5tf9xfrD/QV2v/wHOqv+KT6j/jQyn/yUKsP8+Mrv/nW+2/70DvP/NCL//njzL/xgzyf9IAMX/w/nK//xLyf9Sy8L/Vw+x/6zOsf8zrrv/gJu7/71+pf/ua6j/Yxm1/3Vwuf+Oirr/ELW///cZvP8Ribf/YMi6/+jrt//mQLj/YrK6/+Vsvf9aC7T/tdy3/ylyuv+9xLb///yz/zVip/+OSpn/C/aV/z+BlP9i74X/FJKE/1zqgf/ptnj/Y057/0WkcP+JXHH/wAV6/8WMgP/vBXn/oyOC/wN+nP8zUp7/GPCn/wSMr/+pULX/SD22//RhuP+A5Lz/jLG+/5xdxf/z2sn/P1fN/z391f8wZeL/DXnv/9dg8P/v1ff/iR4JAJbIFgAVfyAA9QolAPRGMAC9BDQA7c48ABobPgBqzjoAqOU3APO7OwB8KD8Al9UyADWCNwAwcTEApQMwAPpKMwAtmjYAfogzAMSfJwDEICkA/lsiAFFGIwDh6R0AppAcAIeMEABN2hAA3PIbAO4xIAB6hhoAPfYLAEDdDgB/nAIAv5j8/xrj9P+QEPT/U/n2/6rz7v9qF/n/9bf7/zdc//+4S/n/YT71/8Tn/v9Y8QIA9WgEALEA9//rzAgAQ3cHALvqFwCH1hIAV4cSAPGRGQBDWRoAkiMrAEMIIQApFSoAuuYjAJdgMgCHvTIATVw3ALj2NACK8DIAfTUtALhMLwAGfC4AH8s2ADqVOADZmDQA4xU7AK1yRgCq1lMAhsxMAC+rUwBzXlUAZfVqAMGKXQB++WEAHkdXAKn3YwCgIGMA6nZXAKGTTwCEOkcAfQBKAG+6QQBOc0gA6odIANRhTwDzbD8AxTFJAHUlQgCqDlcAvrpGAJF6SwAOaEcAZxJIAKvKRQCP60UAiH9KAM/pMQBNUDwA++srAK5ZNAAfUR0AAdcjABsXEQDqbBoAt3wGALFn9/+c1Pj/wBT8/2Ks+f8qC+b/OTfo/0ip3P9Ry+b/L3rV/2Mq3P9WZM//A1ri/1dVz/8ChNb/+c/c//Ea3/+Uc+P/g4vi/9Sj4v88kuH/vGjv/4Dg2f99ceH/CHbS/wL30v8cdr3/gz7F/8Dlvv/9j7v/R6G8/8hpv//E1rz/QaG0/1rHwP+UDcH/G5rU/8C2zP/tkNX/jEnU/29z2P9OGcn/EX3B/ws6zP9x0c7//0XJ/4yzyP9EG9b/RmPW/2qEzv+2Zcj/q0TS/zVF0f/9CtT/tLfX/xTf5f8dgub/ZYPj//mb4v9+WeH/T4Lk/6WL7P+vRO3/RiPq/1Sd8/+hQ+v/Fpj0/xCF9/9eP///kaQFALbq+f91NAAAd7bx/3gC9/9Itv3/fhn5/0R6BADn5AkAGI8QALnhCgBGRgsA3YMKAGzDCgCqiQMA7OECAHx2/f99//P/ODPu/ygB7v9wBvz/ux/u/z4y8v+Mifn/1a35//QZ9P/pou7/kCPr/7/b5P9/4+b/SaLk/zan4/95Ht7/cY7d/w3C2/8Qy+X/T+jn/2c48P+Xtff/kT36/3lfBgBnIQ4AQAAWAEpRDABT7xgA/AQXAAT3HADopScAjSkrAPIxLgCrRSgAmvEtADkfKAAwLy4AvA8qAMhkLQD5nycAEmQhAI/lJQDnuC4Aq1gwAAzDKAB7KisARSApAJb8IAAtyhsAns0eAM74FgBLtRYA7z0gAOH+IQCuCBwAsqkYAH0dIwBd9SMAKcQQAOqKJAC98hgAMI0TAMFoDQBXXxUAD7MdAFL3EgDLlx4AzQYYAMC0IAA60x0AwpEsAFrXIwA9BS4AebkjAFPbJwAR0igAurgiAEAgIwCxrBAAX/YQAEfeEwD6LxUA5QoBAIfnDAAwvw8AicIIAIb5+P90zg8A7mcNAFT2/f8RVfX/6bLz/z6q7v8gF+P/klfr/3b94f/zHeL/283U/wa24P9Ao97/kZXV/1Qi1f/jGc//4J7G/57buv8y8Lj/rBqx//E2sP9fxLb/Fra3/0wXsf87ELr/csSz/+VBtf/cXcT/PibI/29evv+0y7b/Rl2//wYpwf/wmsj/Q5u///d8z/+XDtD/ovrL/2MHzP/xFNP/k/vZ/w4q0v+Zi+f/drPn/9dK6//anOX/JwHz/3EH9/8EePn/4PHx/zc+9P9ta/7/OnXz/2U4/f/G+vT/3iwFALpkAADUbg4AOqQGAHeM/v92m/b/vI/q/xfw7v8jIu3/Bcvz/1y73f9fL+j/CCXn/7se+P/cAPX/UI32/zcA7//iGvH/PyD0/yTC7f8lvfr/lTny/yOM///qDvX/VUn8/2gz/v/G4AEA/54EABEv/f8qVgIAaxoIAI6bEwCFiRoA484WAC3rFQAKjxwABggXAIFJGwCjLR8AvOYgAD/dGgCahBkAPPMXALM6FABs+RQAJW4UAIvLGABzJBEAxEQYADJfEwAutxMAjrENAHLBCQDqRREAyokQANx7DQCGJQQA3xUJABCp/P89aAAAoVD8/2tNBAAXtQgAEor//x/lBgCZvgMAZjsHAK7WCADGUgkATR0LAK2bEAC44gwACOsKACyeDwDGYRQAWpwLAGqKHgC6BxgAN+MZAKiuHwANqR4ANmUjAAdhEQD5ZhQA9TEZAGlSJQDbmhsALY8iAGptJgCVBikAQIQOAHJ+HwA44B8AB+0QAKfjEwC9YgQA/ckKAIfZBQATKhAAdyMAACGJAQBwv/v/DmgLAIMO+/93kf//5gIAAFWRBAC4RRIAUVoJABp7FAAH9AwA6xYfAKWnEgA4tyYA8ZweAPuXIgABRBoALIIgAC+nKAC+1yYASocoAOveHACEKyUAF0cbAFNKIAB2HREADGsbAJAREgCIQhUA8XcLAIekCgDZzwUAt7H8/zn8AQDsYPf/ukP4/7mp8/8u1fb/hvv8/0klCQAUkQgAGicBAN/D7f/LKPL/nzTy/wT+9f9IIfL/LSXq/2FN8v9+eO3/BgLx/yIh6/9xE/j/lIP4/7Er8/8x6fv/1IMAABKG/v/+Wfr/4vYAAOuVAADrJP7/CcL2/8L7AQBO9AMAjzb//332BwAIZxEAGs0SAH8bEQA8rxsAdwwWAHSBFQAQLxwAgtEZACPVCQDBiAwAlnUQAOQcCwCHvAEAhk/+/5oLAQAE6vT/aJb6/wuf+P9lEgoAP1IAAGuC//8b9AQASZ/9/0zVAABr0fb/nPv4/3Uj8/+K/fX/Q532/0BZ+f+R0vj/LnYAAO3N8v+UHwEAaosJAIqaAQBDgAUAKwgKAK+dDgBNDQEA9CoFAHvF/P8CFgEA+87y/3AN/f84S/j/a2gDAIv1/f8Nyvj/1HIEAGG68//GPQEAVH3r/1yi9/+8yvX/rgH//0Yd+P8GX/H/cLLu/9Ao9v8SxPL/+Hro/zWm+f988+X/gQ/p/xbI3/8PO+3/4WTo/97m5f+RYtz/jFPY/39M1f8mYc7/H7HQ/3qW1P/CoOb/ZFjd/y4G4v9/4OH/NxPg/61X4P9GS+T/7Nbi/xSp8/+X6er/xKzr/6538P+4bur/kM7//5p8+f9DewIARbju/z2b8f95IvX/4Jn1/9ezBwCSuwQAYXwOAKH4CQAPEBAA9AwOAH8LGgC8AhsAArEeALTXKAB6DCYA0NknABCgIgBVWyEAksUfABSsGwBBaiQAf1smAFgCHQDAwBYASVIUAOJ6IQAC4CoAM3EyADANMAA/NC0ADuQoAMvsIQB1shsAP+ImAIJkKQBKoSoAvoMjAIakHABINBoAMHweAMSpHQBQnhQAgCURANuwBgD9hQsAXEz//+2ZBgDFZAwAuQ0LABq/BgB4DgkAMB0GAKwJBgB8wgYAfNj5/8yl8f8+l+P/mMDg/7t40f/W99n/5L3p/wpMBQDMDvr/DOrl/+Rp6v8ke9z/1Uvc/97g2//hnfj/DAnn/w0Y4P9pZ+P/cWTp/5Ug6v8PwNr//XvZ/6QJwf/eWcH/wILL/9y57/94jAEApzwCAKkq5v9bYeT/bznF/xP1u/8cLMr/UAjV/5My8f+JKvD/WR/7/74u2/+KMM7/5lq8/2jtzP9uLNH/fD7l/0+Azv/xQMf/Cj/S/+0x2f8Pufb/+s7n/8FF7v8ajuH/Ekf4/3qs9v8cdPv/ppv3/7Mb9f8gQtz/7RfN/66E2/9nwen/ex/7/zzEAgD3fxUA9jMGAMzI5v8yycD/cxLL/5zg1v9Ni/f/Dx8XACqLIADOrAUAyArS/460wf/GMsb/tvDX/09A9f/WiRoAPdwnAH3nJgAjFQkAnL/v/+rR2v+2nen//ZQPAF69HQChwxQAwmcEAI7N/v8I9P7/O+wJADUBGADYMyoA8CojAHGUDQB71AkATwwRAI2PGAB9GCQAOGIyAFJ4MwAgWSIAYoMVAMroFwDkvSYAVX04ABOQPQBjGEEALwJBANksJABvKQoAE1vu/6Mf9v8paAYApiYUABjOKgA2YjYARak2AJ52EwDHwQoAd176/01O+P/WHBYAqSQ1AHsNPgADeTIAEC0hAAwOCgA84woAzo8ZAJM2NgCjDkUAtr9IAL38PgDxIjEAgHkYAEsXAgCASw8ASpYaAPbELgDzMjwAJv4wAH7xJwC/AQwANEgEABrWCgB+lAgAd/8MAHwoBgDtRQwAd7j7/8CT9P915P3/pIcHAFEYCABTzgYApjwSADk9EgDexQIARpPw/82V8/9QNfL/24n6/9Tc+P/defr/fVft/8E16//DD+7/k5Xs/5v67/+Rfe7/5Rbu/+n31v++K8//htHN/+z34P8NYuL/EV3p/xiL8f8tY/v/YTTr/yPf1f9UBt3/8vXb/1QF5//Ew+n/wKXy/1tt9P/RQ+v/srbg/6rS3/8UZuH/9bDz/45B+v8ASw4AWpIEAP8A9P+6XvL/iovt/3Ik7P9Hkef/VQsCAID9EAAFAAoAov/6/+jtBQDq5fv/4xb2/xYs5v+aXu3/Bib3/4R6//+Y/wIAFAz5/8IT+P9savH/a4H7/5rN6f9+Z+//tgHt/+9S6f9ePOT/zWTo/5Ek8v8SJ/b/ken7/z0EAAC6QPj/27jw/w9D8v9IiO7//Bfs/7Qt+P+nXAkARo0GAO4sDQCcyv//Hj0GAG2j9/+QXf3/iMoQAIyQEgCGaRUAQJ8GABxYDgA2SAYAXsYBAJuWAQA5mAYA/n8PAEdZEQBJfRAAWwILAIvxAwBP+AIAqGgIAMz/DwC1Lw4AIPkAAEeBBQBBEwoAnjQHAMWb/P+WT/3//BwFAPjFBwBs+gUAVPv+/0PfDAC+GwYAE7sIAFP8BgBHWg0ATMEMAAjm/P+iyQMATEn6//4gCQAs/wgAQA0NAK6VCACGq/3/uzkAANwuAwC9ZwwAS8IKANRCCwDQyAUA1rMPADUCDQA0kAUAHFIEAKgPCgAwUwwALooFAIOvBQAKQwkAPFoMANczCwC5oBEAYl4OAHq1AgB4NgEAJFgFAAkpCgDRGgkAatAIAKS0DwBQpAkAP7UJAA8NCwB6wA4A9XEBABR5+v8U6QEAxzoCAIv8AABdKgMA3FETABZMDQDqMw0ApQ7+/+jz+v/aPf//Qv/9/1czBAD/7wsAqdgUAHbiDwBaPxIA0uD9/0rC/v9m4vz/GU39/+jA/f+HjwQA/5UPAPu7AADk8QUA3xUJAP/WAQCmevz/7fD4/6Vb7f8HBu7/M87x/ymW+/9nH/v/DM7//1yY//9fF/T/Vsrw/zto+f9Hjfj/H3UEAMmaEACUiw8Atx0bANSDCAB9ewkA7EwAAJBM+P+UnAMAwcQQALiIDwAdAQ4AD2oUAKzmEwCIzgsAJB8BADuMAwAPEQIA9qUKAAH0DADRMAgAuXgMAGU/FQCQuQUAoBr9/133CAB1twoAof0FADj0/v/DFgMACz8OAMF0AQDzTPb/Mazu/wAb8v93LfT/x+fn/+IB6P8iUev/3dju/9xh7P+3U+j/iMHh/+he4P/C4Nz/alLj/0lb6P9G5ef/tFju//w9+P9DIf7/XQ7//1Xi7v9DYOb/k1/j/9g04v8fQur/F9rj/0JQ6P8+qOv/DjXv/+F08P9eze7/m0Xn/8G+5//36Oj/dy3p/0c06v8OXez/ad/z/36s/v9A7gIAgHP7/wNA8/+CCen/2m7s/9Lm4f+Pmuj/8O3r/6S3+f+6YPv/SQD6/2eoAwBZzvz/5+nx/5/i6/8KKu7/f2vu/xbw8f8GKer/yyEEABoMCQA6twwA2ikAAPb4+P9XGgAA/+n4/02x9f9qOQEAtqAJAPADDADO2QQA6IUDAK1FCAASlgMACpEPAAqjEACeDxIACb4IAIRDDQB90AkA3tIRANp3EQAkixUAYvQLAMKwDAC0ZgwAh0kQAPJeFQBJ8xIALB0UAMt2EwA5lRQAld4QAKnzJADqRRwAGHscAIQmFABR8RcAHtEGAA1ZAAAzNQoAuRgSANjfGAB1AggAyycWAImiEAA7AAkAerf8/zH6AADVNRQAe80AAAiABgAnxPz/9aIHAJ7+DgAaqw8AhfYkAAPEIAAqTQoA5EsPAAH9EgDQ3i0A/j4KAPhqGAChDi8AHDkrAGzBMwDaxiAAZLs0AHKJHQCKiCIAigsmADOeJQD6bQMAG+Xl/9hnt//KISUABt1WABivlAChyAMA6Vh8//caP/+JI+b/nARjADYiU/+eQGH/UWLg/3sMyAAOUOX/IOjv/vi1jP+isDoA7ov1/7r2Pv+AS2b/As+G/wYJ5//Jufz/pRrz/lccDAAWHN3/56Ss/sFMjQCP8/QAD57p/puix/0jgZcADWdeARPg0f7V4w3/JAogAC1mmwDqntT/4t+E/zfnKgDmfSQAKpDwAC8qRQA37r3+0j1S/zHNzwAnDS4BjVZrAL0TFAA0bEwAURXU/yBuMQDisYAAAxVtAJiBKAFbmQ4BB+kfAHOwN//4owUAxmLDAEyESwClO4cAxjYbAeaCzwDIRlcAjqpFAKu/ggAhrVIAfxzS/509fQDqnXkAgK9JAIpyFQCt9wEAQjwWAGV28P+mLYMAFu6dAMnofwAh2vf/mPW//01e+f/PojoAo2tHAB1cbgDnMokArok+AAm41f+tv+D/zMLt/6x8rf/jPxsAD9dWAEbj6v9vb1z/T9Po/yb18f+s46b/tYLB/ynptv+jMsr/7Ii4/xRJ8f8by8j/jmJz/2pWX/98WMn/H9MMABBJ0P8Dn9//1b2X/55IY/9Z1XX/0e55/7cOaf9FyJj/x2EJAHTq8f/0CLn/WMqd/9cOnP8SArf/te61/9cLxP+eO+f/cLPf/znd8f9ZubT/XMma/0HUo/8ALrv/L0ja/86zv/+7us7/UZnb/xOm9f8CO67/LTKM/0FFpf+WrIn/OV9//3ZYm/+QZpT/JlV3/yD7w/+l+fv/jJHw/9QY9/+g2Or/HQvH/9JFxf8gXeH/AbHE/ypez//ve9f/qkHd/9Gm3//clOr/NrYBADK5xv+CreL/9Qz2/6oQ+f9NxvT/oHvv/5R69P/aGez/eBPv/zj88//FnAMAK1D0/1fC2f+Q9uz/BCMWAGzACgBfICYAiikmAO2B4P85MwYAA71eAPg7DwCrC+3/Os0sAK/odwC2iUQANbcEADxEUACGtV4AAaY7AJv9SwCSJYwAyW9dAFBDjQCJ4nwAmyh5ADMxhQCi4IIAaj5+AOCWVABwLp0A4vdzAG0mUAACnRgAKHg5AEi4dgBr7VsALBBOAPj3GgCdokAAxl5hACY4NgCXRy8A4jYuAMQ6SgCcSDkAMncyAEePPgCB9icAjUgzAG6MQQAEBDEAVwgxACvkXgCUhzgAPHMXAGCdGQAxAWYAbcl0AIHlIwDQNigA6gk6AGMZMQAdfCIAReVMAHiEWQCdtTkAu6YvABPpRADN+jgA5TwmAL3JSACe51EAGBkMABek9//aqQAAjfbr/yNdAgBlUyMA0EsdAMixGgD17DAAd+QVAAQuMADtXBUAcoctAC56HADzBwYAhsEPAFIcxv/CbRsAe0bv/5qby/9tcMr/6ATt/07cPAA6MrH/fcbZ//40EQDB0r//RLWp/7236f+VTz0ALECf/7gRcv83etD/AHba//3Ohf+xLof/cqzS/8nn0v+xL8//XKmJ/2dBxv9KXwoA/KX9/1m5xf+hLXX/xnbd/zl6qv90kTL/MEu8/9Jutv+kdrn/G8O//2wX3v/CSOb/lBOr//Nmvf+R4oz/2Ey5/4chvv8XCKT/J6LO/2tn1v8az83/hDPJ/0mer/9f7Ln/rLLr/xdt6//0o8T/hCjl/4EQx/8IJ47/OIuq/ziP7f8Zqof/OxuN/zOA+v+hL8X/cu6s/1li5v8xMzwAnSTN/zblt/+LB93/4gv3/3ne0v8ljo//pRzY/z7S7f+M0PX/2Uz3/y0ZLgD5uC0AvBzR/5d47f8dHOT/I4b//9CT8f98eeL/XT4PACIBGAA5JR4Al87N/6F/0v/xB9//zMPj/0Va+v8Akx0Ay7YtAHHfNABkwjQA9booAFfj6v/mlOT/fKYHAJ8RDgDpeSMAMTgZABkKMgDPKBsAIMojAJnoJwDQexAAx3H+/6NGBgDIeQwAryEHAHkQSgB9VzYAINwDAO40GADurjcA3UAjANkNJgA65UkAWzhIAOjnQABocjAAXLg6AJDXLwACxEUAmwAwALOPBQBXwh4A+K9CAEMrTwBvAygA2XMyANFOYQAyqlcAtb5cAH2KZABRfCsAiZMYAHEqQQAKWkwAGl8oAJb2QABnSUMABUgXAMJxOgBVr2sACrBgADB0NQBglzMAb1dlACDgMAC9JSQAK/goAEd6IgBpSiQAvTwQAPMJCwAn9QMAyJ0oAFu66//yCQ8A87obAENCDACDmhQAEnMDAPO5QABYQRkAZ8cHAMqGBQA64+b/Wpb5/+gS6f9YUN//UxoEAFX6GgDUPfj/YjP7/4tIIACxbygA9+YMAHEy+P/O5/z/GurV/xxx7P8M4/P/4fLt/0PM9/8L2QIA3I8lAKdEHAAFrh4AbOH8//8X9f/jwQkALBsCAM1jEgD+3hIAarr7/xYk5P/NM/X/pTLr/w7Q3f9weOz/vXvw/8Bo9v+Bv/P/E1z5/48B7/8XNP7/Lgb1/6oDuP8iKrv/i8O8/y+Qv/8YGur/BvHk/2ecyf+HNuD/TKYBAI8J1f+0qNn/gsn1/xwr0v+p/Mz/3mfd/3GJzf+ib5n/aDy2/3KH/P9dcNX/8nib/4lAx/+pa+T/ch/M/7yrqv8IZOb/SWv4/yN7r/9VYqn/T9zP/4RF7f+3+b//MHHe/xUCIwBHSwUAjTXO/zlPDQCbaSEAvhgNANnL6v9uzOn/hhccAEeC9P8Qrf3/IpL1/+LGEgBopwgABA70//3cFADnlgoA9yHx/9lqAgCUkREAKHn2/9+e5P/pr9H/Sn4IACTgGACHhO7/NZbk/xVE/f+Gfub/tqjh/6F3+P8xEPL/Iy4HAFb7BgCe+wYAoh78/0FS5P/nlfr/IqrV/xEwyf/DBP3/Zjzi/wnX8f874On/cFXs//W1yf87aOf/9EwqACvv8P8rKd7/uVf5/7IIKQBEJff/nS+9/3IBzf8hc/n/5Dnk/1ly2v8nKRAA624NAL1V8P+tpOf/CzcAANj6DwDwhQIAGZMOANKyBgArbP7/hDMGADlcGAC1RygA/AMTAHxy+/83BAEATzkSABlNFgARmiUATlAUAJpxKwBnozkAFg4aAKl+BQAXERkADH8tAJKCEgANdgkA26gjALymGQAaAvz//UUeABlZOQBPkB4AV7zp/9VK7/+++A8AToIPAJa8EwBfdhMAjhUWAEGz/v9NUvn/AE8LAM65AgBpLPr/zD7z/5ey/v8Q5g4AOj4WAHxf/P+43+3/Okz0/yK9AgDkSv7/DNT2/6c+CAA4mgwAF3oMACEiDwB9BQ4AQJ0NAER3BACAWg0AD+YTAHE7DQD0Xg8A607v/xCH/v+OIwsAYCERALxfGwDXIxkAyDsoAK2SHAAcShwAZ0UgAEZEDQCdCwkALfcCAIrEBwDzQw0AZ/MIAMI7FQAqdR4AEHcWAI8VGwCOmSYA8tQdAMr/FwCJlhAA7zUJAP16+f8HTAoAO90HABLyBABdThUAxm70/4vR9P+CVQMA2tfv/4T17P+IXgYACtgBAPPS5/+B5/b/Wvrt/2/M5/+myej/SCDY/yyG3v+/HNf/mSjT/7rk1v8JYcz/o9jW/zrR5f+9Ftz/6D/L/2Boyf+cJ9P/sb7A/0G4tP+tjcr/QPTs/6KE1P/p0sH/3UPT/xm50f+UWM//vDHm/ystCgBB2uX/ufPR/5yq1v+pYOv/bFwKAGmH9P8StuD/P6Xv/znH/f9bxdr/m0zc/10l+P+6CAkAjIj6/9Qg7v/9PvH/0ird/3p57f+Mver/uRwFALxhDADirv//NlX4/7XaBQB+Dx8AbOP+/wncGAC+IhMAsZ4eAMQcLgDyqRkAMQAWAEZMHQD9oyoALD0GAKizGwBz1ToAmfMRABTU/f+4gxcA4K0nAMD5EQDmYxEAsf0oAEm1FwDhzxcAok8WAIcIFwAV7w8AYcAjAENTKQCPSPn/RjAhABtkNgC2/ScAYKkNAFtdGABjJSEAzH0HAMAOGQAFAR0AlBotAMHWJADxkgIANeQSAFwVJgDIzQ0Ap7sKAFpg/f+2ZAkAciMNAL0tFwAbBikA0uYNAHLAGgA34hsAiwwtABiQEgC9K/7/bxYTABGqJgA/GyoABa8aAL10GQCMtycA+aIfAJ+SFgBIlSMAopUbAAXrHwACbyUAmac/AEuxFgDO3wcArQIjAPNZJQBpyBYADWcLAJRhIADxrRkASXceAA01GAAlUAsAXsMTALvrGABJSg0ANDoJABV+///p/AQA5JD7/7v36/8gOvz/iGH1/xCr4/9IKOH/VxHy//7S9f/5Fd3/01HL/2EN5P+RRun/EPPN/8L0zf+9keL/le/Y/6M9wP/eX9j/F1zk/yR72P8nQeT/9KXK/yC7v/8mH83/qiTv/5fG7v+ZptL/iSXx/zqC6v+hc+f/R+7k/4j07v8gfeT/le7s/0hk8f/F+Mr//f3y/2BD9P/2m+3/k5T1/xtx9f8FSvv/v2fb/2Ht8//SMvj/Zd3j/1uk6P/hSuD/pl3Z/8619f8CTBgAVRn//wbV9P/jWO//eJ73/0Vz7f9O6Pn/VA0DANSD9P+bQQIACEL9//z1+/+RNgQARzUDAO5AAgBiQf7/11MJAF2KEwBoZwEAwt0AANS7CQD3ACUAwV8FAIGY/f+TbgIAH1f2/+WGBwDofwoAvBL9/1+U+v86kiEAMR4lAPgUBwC54hIA3xonAIErFABMeCEArkEtAABPLwD3UxIAiEEMALFEGACDzB4ABZUwAOO9KgCuIysARycVAL/OEwAUViEAY84XAKJWFAB/3SMAAkknABzsGQAVCAcAGtsAAFtIEQD2vR8Ao24bACLICwBAYRMAMBYkAEmjFwBk9AwALWEYAIGbQADZsycA5Wr6/zaACgC/0wsAgfsFAG+Q9v8BpBIACi8NAJrZ/f8itff/VG7m/5VL9f/Y+/n/JxgBANiU6P969On/cqzx/4zrAQDFQQAA/yXn/++b3P/gxvT/+2YEAFVH3/9hMOb/qof2/zh29f/Ou+r/rvHr/8Gk+P+2ROT/W6/k/4oR9f+Zv+z/+RTp/8u74f89OO3/9mvj/x0o8P9Od/r/ZsT0/8bq9P8prev/5Vjy/6ym/f/ZbwAAeSb2/yJq+/9PMv//b80KAEqTCgCKrhAAOk8JAKnf9/8PiAkAA5oLAKFyAwB6qwgAEusAAO+wDwAMlgcAnpUPADbsHwBPoBIAJwYSAJLxBAAYUhMAeUQCAO9LAgDniBMAbCkVACRtHABhLv//Kln8/8+QBQAugP//0XLy//zn6/9USgoA13EWAIDX/f9No/7/v+ACAA53AQCdLP//ZRHy/1XC9P/Z0e//QH7o/93f6/95YvD/phXu/9sb5/8Yk+r/VWvp/16U3f9i9t//lA7v/9Mk+/+VzPf/mibv/w+S//9+PwsAioX1/71v3P+vnuX/Kijy/1OP5f/52+j/tYr2/0E1//9xjfb/Ulnu/8IMBgCXpAMAr7/v/z+SAABfJvz/azL//5X2BQDOqAcAkj8BAJGW+v9HBgUAqBj1/ycLDABJLgIAM8T7/3+3CgBCevz/3An4/8Yk+/8nGwQAShH1/w2f+/+5UwIAHjn4/3Bu/v+TFQMABQ0FAGnKAgAO8AEAFI73/1WP9v/t9vr/2DcBAIIq/v/SFPr/qcsFABLH///6uf7/CBr1/9oe+P97YPP/Ogn7/6wV9/8swOX/kHvs/4iQ5P/jmub/XSvV/we12v8nr+b/Tlje/8Cb7f/IZ/H/ogPk/8byzv9TFuX/9Dvz/0m61/8tX+L/nv/l/7Ov7/9Rrdv/sqHg/66J8P9shd//csfs/2qh7P/zpuj/nQfT/yR43/+4q+b/AaXh//Hw4f90+tX//9bf/3YL3f8u6df/pMba/9j13v/mWOD/6m3b/wvS3P81adT/w2Ti/5/76v98l9f/Di3r/8gz9/8iFPf/BDXs/xup5/8n1fb/rA3z/+ca8v+Np/D/OAwCAFwXDgB2Xvr/j4b4/wkfAwCdkQwAFlcHAFxtBACJcAIAOzIKAB3nEwBZGwQAf5EDALm7CACH1g4A2XAOAIPVHwB5dCIAKf4aAA8dJwCzMyEAbGgiABkkGACCpxUAsW8gACo7GgDHJyIAULUjAOOjIQCd3TAA2LosAN46MgAyTTEAV/8qACt7IwDrTR8ABEksAF5TJwCEPyUAIgYbAD7qIQAvjCwAJMwlAJQXJgBe5x8AlRouAEASJQDtRBMAhJcmAK4zLgCC6isAaDoVAIW7GQC5oCIA4ywbAHMQHAD9ZR4A/D4nAJ1jFQAOIg0AyaUSAIxOFQDt7xsAYicUAJYtEgC4UxQAKZ0TANHPFgCvpQYAm+8JADW2EwAU/wMA1F///3G0DQC54QsAthgIAJJSEACenAwAmp8HANTnDwA2+Q4AV0IDAL+h+v9gHP7/TiT7/waB/v9pyggA9GL5/99W9/9nIfb/jvb2/9Ho+/+Ch/z/PDT5/9F08P8LTfX/gzL7/67x5/85L+D/QRrs/3Kg9P8cBPb/WLb2/wOY+f9bRgEAJfX8/5IX9P+V3gkA26QGAKo9AQAdRwEAYQT5/wxZ+f+3R/X/1Tj5/6F7/P/oRwEAf/8CAEPwAgDDT/b/cS32/xSgBQAr5AIA8Fv+/5UW/P8NjQcA9MUFAKT4AwAYbAQAoHb8/5G8/f+hI/3/Yfv8/7X3+v8advv/y+n+/8MVAAAfYAkAY2kKANyc///d1wAAo979/yBIBwCJvQUAQPv8/6A4EQC0txgAYmUPAHKOCACuIxIAnuYYACZICABPAA0A1RgTAB1uCwCKGA8ACCoQALRYEAAXwv7/9bv9/553CgCACQoACkICAG8hAgDOmgwAou0SAPleAQBJtvf/H3sDAEzB+v8sc/3/fUL7/1rW/P81zvX/kZP7/5nB//++GPv/Y6oBAArm+/8pIfn/5CL4/4UPAQByGQYAmV0JACRXBgDoyAYAGN37/z8o+/9hBQAAvq0BALnoAQAtjf//RKgJACDuBgDU/fv/gp0DAJXiAwCsDgIAKXcGANj+/P9rug4A8hMIAG5eBQBzDAoAwpAEANdxEACqqgcA24gIAGZjDgAacgkAYaUFADCzAACVTgsAxjoNABPB+f+rkfX/7j/9/702AAAac/j/+4v9/5rB+f8izPj/G1f8/zUN9v//Gf//40v1/1Lf9v+r9u//srjv/9zV7v/fdub/lUv7/z6R8f+jC+b/Dcjr/3ee8f99W+r/QB/j/8yE4v8Pd+f/6XHl/+lN5P+oy+P/TnDd/6Bt2v/fftv/9XbZ/9Ee4v8yzej/JyvV/2fR1/85YNf/0P3h/8qp4/8X6uP/Ypjn/yek4v+I/uj/imXp/9fr8P+hwvD/8NHr/9gA7/92vvH/Apnt/7xF6v+sOe//aT71/29M8v8rcPz/q235/+4+9f89Mf7/OTr8/18xBADQmv3/5HQCAFVbCQD2KwwAk9ASACThDgBhtxkAJ/oYAMmdGAAX2xcA7YIUADJJGAByChgAvy0WAH+4FwA/vhEAnnETAJacEgBysAsAaHQMAKwECwD1VAcAx5EDAM6UCwAk+gwAU9QIADnlAwC4wA0AjYADACSo9f/lRwAALgQGAJwRBwCQkgYANxUPAFqXCwCGZw0AqsQHAJOwAwDKwP///Rb5/8iM/v+SXPP/XZ79/9hU/v/MVPz/55sAAAVGAQDD/AAAgd/7/+X5AACumf3/nd/5/y29+v+xW/v/Gsn3/0jg9f9zfPX/jF/2//F/9v9qjfr/2ivz/36L9v9y6QEAuZ8CAHx3+P9GOfn/fQEBAOWf9//sNv3/C632/1+R7/+s2vb/+IL3/3XY9P/uHPH/+wv3//Lw8/8N9vD/kWv7/7dZ9f/uWPP/9Gb2/7KQ8v+HIfP/5xX3/9St+P8Q4fD/Scbx/x8C8P+fcOr/H4Hv/7tI8P9w6fX/grzv/2zB8v9K+PX/oWXx/4mI8f+T/+z/q/72/0Dr7v86oO3/LvD1/yIq9v+CmPr/fl/9/8M//f+vHfr/O/f1/0L69//3G/f/Sg/1/2qj9P+01Pb/lDr//04r/P9vsf7/TX0BAOP/AQDUVP//36D+/2qVBABHGf//MEX6//gR+v8lLP7/boABAOtL+P8C/f3/FrQFADhf/P9itPj/fon6//TO/f/OxP7/9k/z/+kR9//i8v//0Cj6/1Pa9v9RUAAA0jsLAHzSAwDg3wcAC90VAFx3FgBISQ4AoiQWAJccDwAndAcA+KEGAAXeCAAXRAsATVcDAK4GCQCmxRAA8GYZAFufGQCOBh0AIv4UAL+FFQDyFRQAVm8TAMHoFwBj+xYAkIocACeuFgC1HhgANsQWADRNGgDpoBUAqckWABhLGgBdUBIAnvcYAMakFwBwXBYAY1oTAANnFQBTACAAPXscAMuwHQAwDCEAj4IhAJSlJQAWUSQAQQckAMAJJgAMGSQAxLkdAPQtGgAcoRkA2IAYABZsEwD0/BgACYsfAMeOHQDIoh8AoRIhAOh7JQAmPRwAzxgZAIs6HgC33RUAS3ETAKpjEQAooxMAjtgVABt4EQBKWBsAEGceAArRFgCYYRIAo9kRAH2WEgCarxMAzyoTAI1zDQBF5xIAVNUQAGRkCwBNyQMAC+YAAKfvBgAQiwAA3CX+/8L4+/9XVv7/PksBAOOw+f86Of7/hef7/2Gh8P/eRvT/oAn1/0jC8P+cxu//+c32/wE28/8yIOr/2Gvs/wdP8/8Jk+//swbn/wqk6P/I/Oj/mgPk/zr63v/TZ+P/cjHj/wZS4v+3Pd//0Cfe/yhW3P9dkdf/aH/T/5Awyf/5bs//KgLW/7imz/8s68//4AfU/49T1P87+tX/mc3V/31q2v8Kldn/y2rT/2ky0v/kpdH/SLfT/9s41f/WKdr/cOLc/26d3P8Wgdz/iIjl//+r5v82D+X/Kezt//xd7v95Gu//T2jx/38D9v+EU/X/nXr5/5mY/P8jkP//UVv+//qkAACw8wEAI/L1/5sR/v9c8P3/Gm/7/2IT+P/BigMAJd4EAPwP9/9PLf7/AdABALy8/f9r4vb/kOf8/0ut/v+3nf///f4IAF+8BwAEAf//mkIEABI4CQAICAUA8+L+/84LAQB02wYAZmz9/3wh+v/TPwEAKtP8/98U9//bZPT/wnMAAEA9BQCBZfv/5DYAAAeDBQA1IAMA7Gj7/45N+/99yv3/Otj8/5yr+/+cm/z/Ok0AAJrLAQAXLP3/PlwBABMUBwCkdQ4AT2UGAH8WAQByzAYABfcEAO07BwBc1wQABCsJABa7CQA7OwUA+qUGAEvSCADyYQoA3EkNAAZPEgCu7BUAlM0NAEJsDAAa6AwAu9kMAJqlBQCv7wcAQ0IGAJ1QAQAVMggAQQwEAOg2BwAhSgkACnQPADm9CACBfQUArnoPAK49DgA2bAYAkQoAABQkCQD25BIA/l0PAPLCFAChdRsA1/4bABjsEwAYkRQAoZgdAA4RHABbORwAJeEWAJ10GACN1hsAMHkaAJjeEwCrkBIAHi4SAMqAEgAGdxMAbnYRAIABEACNyBAA82MTAMO1DgDDFQ8AN7YKAIYDCQDqvgYAeGkKAE04BgDZngQA/vIKACkkBgByUAsAqNMEAJiEBQDSNgUA0V36/61i+P9rKfn/JHP8/8wU+/+8u/n/6Y0CAHVvAgC+vv7/+IwDAMPDBgAo4woAyUEHABIvBgA0eQYAHCcMAIAOCgBTJgsAdpwTADS6DAB35QoApgEMAOPlCQC9KwcA0DwKAIhrBwBzLAIAuCkDACLpAQDbD/3/aAj+/wm7AQAsygEAsdEAAClnAgDC2AIA1s8CAIFbAwDbT////h4CAMdmAAAOcwAAZ1wEAJVQBgCSOQUAQLL9/+seBQBBpwkAe4n//wmz///cIQAA7N8BAFbG+f+u8PX/58cAAFgIAABtzAEAY0YAANoUAACqpfz/b+D0/6gE+P+HMPr/Xkb6/4EW+v/D/Pj/6jD5/38V9f+jqfH/7n/0//BL9v9j5vP/u+fw/35T7v/cT+///Pzo/wQd5f9VHOX/FQjm///M6f98C+T/xb/n/0bl5f+svt//NuTf/18C4P+VVd//GbTc/4rL3/8TcN//9sTa/z0g3v9w8tz/Wtvd/zT14P9A8tz/Eyvk/wBy5v+R4+r/U+Tp/3wm6f8+1PH/EFzv/2Jq8P8rIvH/kkf1//yG+f+6/vj/h3L9/0TG//8srv//P+EAAF9OAABn1AIAdfAEANkeBAAO9wcAXE8MAOyQDQDYdg8AVMIPAN3cFACqXhkAI5UZAORwGwBC/xsAtTkhAD5aHwC0cBwAUWEeAAAUIADnFR0AQccUACleFABDQBEAscMPACfOEAA1hhEAY5QRAF8RDACk0A8AqtsMAEsXBQAo0AMAjZEEAH3iCgDifgkAew0AACqvAAB5XgMAmjoBAHxg/P/abfz/61kDANOxAgD1aAAA9mv//50RAgDiOQIASz0CAK9mAACkFwMARWIEAMeb/v+xUAAAvEv+/zrJ+/+jBvz/aF0BACzDBgBUVAQA1oMCAGuH//8ij///qm3//31t+/9Un/r/AE7+/62UAQCwQP3/nhz+/7M4AQApifv/r7H1/7ob9P+j1vX/mJn0/yfj7f/lVe//SOfw/9Sd8v90VvD/wIjs/+HP7P9mvOj/QQbn/2p15f/7reT/ST3j//LB4/8nhuT/8Tvl/+Dw5//++ub/TYLk/33g5/+jpOb/ZHfn/2Dq6v9N9+z/Kuzr/w3Y6/9wo/H/+an1/0R/+f8N2vj//fH9/+W6AACzWv3/Wl79/5QXAQBFHf//tIX4/6Yl+//i8f3/63T3/3az9P8nWPH/RDzz/+j98/+4We3/P9Pw/4qn8f9vSPP/dH7v/xMR7P+hmuz/vKDo/7hx5v8zKeb/LR7l/+Rm5P/Dxub/2LDp/5E87f/47+n/eqDo/7D97v9ggPP/RlDz/65Y7/9wL/L/9ubz/3lO8f9FP/P/oTT2/4nv9/8qd/T/tsT2/5NH+v906P3/OxUEAH3XBQBq/wcA/oENAJO+DgD9nhIAIfoXAJoiGwCxJR8AkBYdAF0yIwCWRCIAlXIdADYyHAClBCAAJX8lAEN1IADbLyEA/gUjACvDIwAicSYARhcnAGwEKAAOMCUAhFwpAED2KgCFOiQAKNQkAC7GJAAwAiMAhWcbADppGQBgIRwA8uoaAJkJGACznhUABRgXAAk8GAAUTBYAjcgXAC5FGQD0thgAeAUYAFKbFgDpMhkANmQWAP7RFADsaBYAY4IZACKPGAD0ZxgA7mcZAGtOGQAI9hcAH80VAKiaFgAGqxQA3S0UALCaDgAMdw4Ac8QOAJGYCwARcgoAvBIKABazBwD5pQQAbLIEAFmpBgB2tgUACq8DAJgjCACTtgUAdcUIALbQBgAH5AQABYMHAN08BADlJQQAZh8EADgeCgCWKQoAosIDAN5CAADD7gMAHQQBAMy3+v9E4fz/p+P9/6K7/v8V3Pv/fV38/56X/f/um/7/0HIDANHKAgB3igIAR+sBAJ1GAgBTGgQAiIMDAK1ICAB7gAgAAJEIAB98CAAAFwYA/tEKAKXNBQDbIwQAwmsCAJiFAQAT9QUAO00BABA///9AGP//B7wAAJcHAADJS///6sb//xpI/v96Qvr/Rgz8/0fW//9MxPv/Gz78/5Yu/f9u6fn/NtP5/8Eg+P8wj/r/94f5/x1R9v/a2/r/zA/+/w1RAADk5P7/BUIAACblAAD1XQIA/1kDAI0FBQDfZgcAVvQHAGBsCQAtCQkA/tYJAJsxCQAk6AUATREEAOGOBgAFoAQA7dcBAE6O//+09P7/YxP7//n3+P+wQvn/skv3/z5e+P/3Q/L/UuHw//cR8v8tYfP/8MLw/9Lv6f/sP+b/10Xi/50U3/+Eatz/is3a/yyX2f/Awdj/9cjT//3R0v+1i9T/FerT/5zJ0f/qVdL/9yHT/7Fzz/8ANM7/OjDQ/xvA0/94hdP/TUvT/9wb1/+fHdr/GJ/b/3s32//u/9v/SOTd/wnd3/+RU+H/N4Tg/xgU4v+fY+T/eurk/1QR5P9mYeX/+ETo/1sZ6P9i3uX/YGjo/wG66//H2er/V0bs/yu77/+Re/b/Ovv4/xRq+f8JpAAAX7AFAAf/BwAe8wYAq7kJAGUfCwAWFwoAMioIAL18BgBbrgUAYc8CAH4kAgBjsQAAS8wDAIZv//9wBgAAOH7//zul/v/G5/z/1cT8/+VZAQBgOf//x1wAAE7UAAASZgIAc3UAAKgQBADccAUAx2AIAJKVCQBWDwsA2rUMANAQDgDGSw8AK18OAAlXDgCtFw4AJYASAOXADQCjwgwAdeoKANkECwD0bQgAUUcHAJnQBADa2gIAhXAFAPoSBQCijwgA50QGAPwpCgCBPgsApRoOAJMAEQAPQhAAcfMTAOLbFgDiWRgA3m4YAGOlGABLlhcA6CUXABRKFwAH8RgAvGwZAMajGwC0ih0AqQYgAJ32IQAgySUAT/wkAK4yIgDlkCMAU/IeAE4NIgCkASIAFpAgAOUFIgBN1yEAo24fAMOqHADuvx4ACn8gAA+XIAByGB8A8yofAIEuHQDmGhwAz2cXAGGMEwDqIxMA5ykUAGxAEQDdGg4A7ZQNAP1PDQCYbA0AGGQNALv5DADA5AkAjkUJACgpBQCyGgcAczwHAKxNBQAaSwIApfT///x5AABe3v3/QcYAAAisAACErf//ylj9/4OC+v/8mfj/P771/4kc9P9M0PL/xWLx/6Ij8P8do/D/Sa3x/5Ac8v+QOu//MR7v/+XE8f+lLfH/z7rw/37B8P8c4fD/SePt/zcP6/9kaOr/MfPo/1Or5//WTuf/4rLo/1jg5v/AjOX/rEzn/xd86//cT+3/GZXr/z4Z6/+gAer/+Rnr/0sG6/8pX+7/gTXw/3sz7v9tNe3/gZvt/+4d9f/6p/X/e1zz/8uU9v/6wvb/r1by/0sr8f/qkvP/gJz2/wSD8v+b1er//XXu/0Tt9v99ivz/dOb4/4D88/+OCfj/MSn//3tiAQCcGQMA1GX+/1uD+f9TSf7/SDP//6N7+v+NZ/X/VMv2/4iY/P86FP3/Zz76/5JW+f8kxvT/TzT3/241AAAUNwUAeW8DAN/4+/8ix/j/tpb6/yArAQCfQwcAlmILAISbCQB9YgMAg4sAAJf8BQBccgwAM+0OAFWXEQBVZBAAC1oOABjvCACrzgcA8UMKAEgNDQD5tAwAuzgJAJTpCQBOnwkA570KAKuRCgAezwoA9XcMABaBDgCoMg4A7JQLAHnwCQD3JgoA4AsJAOn5CgBu9g0AkswRAJ+PEgDc8wsA7b0MAJBVEQAj6xYAkvYXANPzGADVbBYAcoUTAHvSEgAdTBIAuAoSAKcdDgBV9AsAChILAGejCwBmZwsASrQMAFNACQABSAQA79MAAIkNAwCviAAAq0f6/9Lh9/8kjvb/Lg31/8L28P+4ZvD/PU/u/8bX8f9iHvH/U0fw/2S67v8Vt+v/QKHr/+YL6f9l2er/HiXq/w9O6f/THef/PqHk/xFj4v8cFOD/8lXe/wIG3/+pO97/SwPe/6dn3v95Ft3/dvvc/0jV3P9Lqt7/n8Dg/wmY4P8eQOH/4wPn/xdH5v/jPuj/qFLq/0gR6//kX+r/9UTn/4VE6/9exO7/HOvv/91n8P/qBPH/J1fw/w8Z8P//9PD/5i7y/9xg8f+Kz/D/8Gfv/9Yy8P90J/H/WGHz/xRx9/9TA/v/GRv+/7a0/P9bM/z/qir+/8NP//+wYwAAlMMBAGPRAQC/hgEA2ZYAAAuCAQBH2gAABogAANHPAgAubQYAKoALACSyDgAt7A8AWxATAP3yEgD+xhEATIQTAC4DFgCRIBgAk0QXAH/6FgCoaxcAJXUXACT9FgBoDxgASCoYAGSPFwBGyhMAnCsPAGaMCwCjFAwAElAQAPduEwAnfhMAKiwSAIWGEgBzkg4AgT4NAGDdDQAgvA0AJdwJANgUCQB7bwsALiwKANHxBwD16wUAwdEFAFHfAwC+YQYA6cwJAC5mDABUBAoAtEwGAJ0BCQAK8gsAI44OAFYyDwDRtg8AB9cNALdBCQCvpwkA4ZYLANkNDQCt9wsAJ34JAGOpCQBdegcAZ+MFAOHvBQDrTQgAsfEIALqJCAAxiQkAcMMKAJ1cDAD++Q0Az4QPAC3ZEABk4BEAHJsMAIHWCQA56AsAkNoKAM79BwAabQUAWf4GAGQUCQBB9QgAUOEIAFzXCADtnAgAwZYEAETvAACLV///6XT8/+Ek+//I1vn/YF/6/9uK+/8OH/3/ZYL+/7JkAAApvgEArJ/+/3El/f+8y/3/Nm0AAE1OAQDsTgUAlxgJAKocCADo+wQA190EABHZCAD9XAoAZkQJAPmMBwDO/QYAMpUFAESsAwBVYgIADjYGAHayCQC9KQoAxkULAOfXDQCXag0A0+ANAFK6CwA7VwoAbJcMAJBiCwC3CgwA//4LAPxNCgBLogUA6jcDAJQtAQBQkv//UGMBAG84//8fhP3/S/D+/8CI/f8cDP3/XtD7/87e+v/NZ/3/Rlj8/yGo+/+Hu/v/cAT7/+Fd/f9sV/7/m9b9//+f///KQP//9i38/6Wh+v8A2vr/TOX4/wMw9f8GyfT/zVj0/wc99f/OgPj/AdT8//OPAQAlJgEA54AAABQ9AQBFIgMApioEAO1GAgAC8QAAaJf8/z3Z+v9HAvz/y379/9N//P956/n/DO/5/+O29/9+DfP/sWHv//YP8P/J2ev/omDp/6Ut6v/0Oe3/7GHv/89k7/+x3/L/gEzy/6yD8v9CzvL/ONby//jg9P+HX/X/41r0/8r48v87f/L/WGrx/+Ez8P9P/vD/fMLw/2lq8P9jRO//0czx/xta9P/NRvb/PTn1/xz58v8JwPL/9kTz/1ys9f/P/vX/gu74/xKI+P/fIvX/8obz//6i9P9AfPb/3u/4/2YK+f8WDvf/8A71/47m9P9yovT/8j30/wMP9f/K1fb/4hn3/yu48v+Pl/P/D7Py/62K8v9KFvL/hH/y/6XP8/+lAfT/Tbf3/1P99//AmPj/wgr6/2Kh+v9pOPv/L038/9gY/P+skfz/cOb8/8lN/v8Ksf7/VWX9/y9P/f/dyfv/Sf37/20Z/P/Z6Pv/znb9/10i/v/jBvz/dy38/wVI/f+qBP7/MLP//+VzAAB3NwMAI6gDAJaFBADiAQUAm6sFABb/BQAWoQYAzlsGABqgBQAdygcA1n8FAE1MBgCh7AYAhhIGAMP7AwCP4wIAhy8BAGaGAACwewAAGg4BAFhcAwC4bQEAnZEAAHUl//9kR/7/WXH9/y2T/f/qsf7/TOz//+vN/v9ysv//qrUBAA1QAgA0lgEAI/gAAM+YAgBp/QAAdGL9/xyN+v/hV/v/GV37/+LC+f+Kpfr/xlD8/5al/v+sXP7/l9T+/2e0AQD+3QEAeMMCAEIXBgAOmgoA5QoKAA7RCABjegoAMiALAIcLCQALnAcAWJAJABtVCgAkzAcA+fEFABAPBgDRagYAY8MDANEEBAC1AwkAhlkKAB9tCwA0sQoA9eQMABITDgB8ogwA1lkOAAZ/EQA8fRIATvUPAPbuDgCdLA4Ak/ILAAS2CQDfIggAsWAJAMLBBgD0AQMA2JICABg3AQDi+wAA0Wn//9saAwCYogUAiOEFAPhLBQDcJQQAHXEEAK65AgCoAQMAOx4EALNJBQBf7QUAT9AGAD7HBwAROQoAdJsMADa9DgBGcRAAFwERADe0EAC2mA8A8egOAEpVDwBS/g8AOi4PABHuDQBWgw0AZHsMAJcrCgAvtQkAWJcKANBPCADlCwYA7PYEAIn9BAB0EQcAaMUGAESLCAAqvwsANawKAFcRCQDy0AkAAUIMAFgIDgCz3Q4A1/kRAMhxFQBd1hUA8xoUALotFAAhcRMAf0YQAEAyDACiYwkAv7cGAOi5AwDdjgIA88oCALsKBgBsHQcA9+gIAOetCAAm9AYAmv0GAAJJBADqYwUA4AsGAJtdBAAu0QEA7+n+/9Ax/P+F//n/64P5/w/3+v9vO/z/HmL9/5nB/f+pb/v/kPD4/w059//OePf/sej2/7VT+f+QlPr/l2f7/4UO+/8Ri/r/UPT3/xcT9/9/zff/nDD2//739f+ZY/H/hTHx/28h8P9JGu//RqPw/y4M8v9ufvL/r9Ty/+Et8f9qY+//QyHv/yGN7v8SLPD/8qLv/+m78P/kWPH/Zhbw/xgy7v+BjO7/dOrs/9G56/+69uv/KLTp/3+E6v8nD+n/3Czp/6W76f/doOj/xjfp/+DE5/+4huf/Hn/l/5zp5P/+ruX/ddPl/4v45//Na+r/no/v/5Oy7/89w+//2//v/8aM8f+TJ/X/jGz3/w2v+//s+vz/uwMAAISHBAApyQcAfs4LAL2zDAD/tAwABXENANvBDgDQTxIAmE8SANOQFABjcBUAfn8UAF01FQCUPBYA798XAE/+FgDAYRYAfEAWAJWNFwBB4xYAVVsVALtiEwCtEQ8AcNwLALVYCgCabQkATS0KAOd9CgCuEggAFTMEALTG//+h3fr/S6D3/68x9f+VCfT/TVz2/4X49v87evX/tnT2/87a9//KJPj/zB/4//IN9v9CXfT/bv7z//Q68/9CPPP/Qxb1/4zp9f/2I/X/OKTz/yq99P8dl/j/E6b5/yrQ+v87Ffr/Srj5/3C0+//suPr/koT5/4WY+f9Jcvj/QhH2/6w+9v9m5fT/Q4n0/wlC9v9/vfX/SQ/2/1Mq+P/Kp/v/GVb8/+So/f+JdP//ufv//85e//95ewAA0qUBAA0YAQDUBAMAMsUEAJ0NBgB6LAcA8+oIABEVCQCQTwoAWIcKAFZ/CgBizgoAZ3IMAM2tDQBB2Q0AdG8OAKPwDQB7aQ8AFr0NAAEhDAC7XQoAf9YIAP21BwCGMgYAir4EADHdAQCIHv//lRz9/0VO+v/smff/u2X3/xgG9f+zgfP/XbHx/2T07/83wO7/kTjv/9wl8v9vF/T/lmj1/0VI9P8qEPT/wE3z/6I08/+e8PL/aC/y/63K8v+GAvX/bf33/zvG+P8erPr/J7/7/3yx+/8Z/fv/VSn7/xbQ+v8CZvr/tvb3/1Mx9v+4uPb/qMf2//qd+P+fufv/sBX+/06M/v8FQv//yP/+/1F4///+AwEAG4UBAE80AwCyvwIANpUCAPiS//98svz/pFb7/zQM/P+SRPv//0v8/wZnAACEzgEAKqoCAFylAgCqMQUAKCEGANAgBQAi7wQAZM8GALXbCABopQsAUcoQAN/EEwCIvBMAT3IUAL9dEgAO3RIAlUATAAUeEACx2g4AlLcMAK5oDAB9JQwAJg0NAOBaDwAjzw8A6awPAL9hDwD7oAwAJ8kLAKWjCwCMagsAHCELAEFhCgDtJgkAPI8GAES7BQAj4QIAkW7//0q9/P9uoPv/tzz6/7NV+//je/3/Voz9/93X/f+zSf3/3rr//0NGAgAL8QQA6fYFAN77BQDxWAUA6MwCADudAwB+pgQA50kIAJD4CgDTcAoAkTELAPnlCgDXNgkAn0AIAAGqCAAD9gcAPo0GAD2iAwBEEgMAYtQCAPMxAwAPkwMAXn0CAFIWBACH2QMAETEEAOkXBAB5ewMAbFgCAOzsAACJhAAAyjIBAOJ2AQCEDwAAOi8BAOkvAgAOfQAAj04AAIqy/v/csv3/B//+/5WS/P93Nvz/4yv8/1f3+P81iPX/Va70/5S+8v85WPH/xOTv//Hv7v/q7+z/r1vr/8mu6v+hvuj/1b/o/9kz5//IsOf/8Vnn/97z6P+isur/koHr/4vD7P9T5+z/S23t//eK7f8mL+z/DHzq/8ru6v/OH+v/Tqzq/0IH6v/f5ur/AWvr/8OA6v8+Euz/zj/t/w+W7v8cGPD/DPPx/we69f+qufj/rT37/3fr/f9BKAAAiakCAOmlBQDHqQgASGUKABwtDAAXRQ4AGzgOAG6vDgBb+g4AU8YRAILXEQCVCxIA6w0TAFlWEwAGRRIAL2UPADDsEABtGBEAoBIQADaVDwARERAAy7YPADFQDwAaAw8AVhAPAFGiEADcMRAAPEoQAETXEADfmxAA2wQRADy2DwD+fQ4A/xgPAFEIDwBKbQ4AvFENAEf9DAA4cA4A1EkNAGeNDQAlwwwAq7EMAF4qDABqNQsAB2AKANNMCQCKSQoAVJsJACd7CgBXUQoAYZ8KAPU5CgAgxgoAVAsKAJliCwAIhQsAWpQJADPvBwBKegYAT8cEADmcAgBTtQQA4isHAGcHCAAuHAcAcm0HAOzmBQCFMQYA5pMEAAdBAwCIiAQA2pAEAACyBACMmAQA0XAFAGNvBQAyYwMAT5gCABp2AgBfFQAATzr+/9B0/v9p7f7/GxL///BcAQD+nAIA+EIEAN6EAwB0aQIAC+oAAI2G/f8BU/v/xZ/5/zYd9/8SgvX/Sfbz/8Bo8P8wQe//Nx/t/wit7P/YGOz/7gbq/5bF6P8+e+f/l4bn/0f25/+9eej/7uLp/4V46f9Diuj/aAHp/xXT5/8GvOf/tXLo/5dB6P/vO+j/RjXo/53S5v8ga+X/HFnn/3NE6v+F7ur/dvbr/80W7f9gyO7/Tmbx/4+48v/ozfX/PDr4/5A5+f+4DPr/bAv6/3pn+f8tMvn/tj/7/3Tr+//FB/3/oIX+/yZB//9aBgEAPdkAAK1+AgAUJAYAISQJAODGDACQhw0A0FUOALOPDgAjBg0AhZMLAPrMCQCGUgoAu/MJAOopCACGhgUANU8DAMdOAgDKkAAAOvT//yRm/f/1F/3/eeL8/8BB+/9vzvr/rav7/5RI/f/+9P3/KmX+/4tD/f8igv3/wJP7/6WJ/P/C2vv/m2v8/yuO/v/Dzf7/6xIAAD69AAALwQAAQXkAAMrUAADCawAAS5EAALff//8LuwAAdwMAAMW2AQCYywQAKz0HABYQCABGqwgAU3MIAEveCABU4wgAEt8JAKD7CwCQZg0AaCMQAAeFEQCk/RIAlyQSAF0qEgDkyBEACKcSAHkgEQCB0w8AuGUNAFJIDAB+zgsAqdUKABu3CgA/TQoAt7MLAEroCwBdvg0AwkkNALAfDwB/lQ4AJXIOAP8wDwDFQw8AMK8NAAlhCwDuIQoABeUGAMLMBQAGMQMA5n0EAEaaAwBm2AMAvSUHABhbBQCHHgUA1TIGAAQHBwDN0wcAvd4JAEobCwCGQgoAcFEIAK+9BgBxPAYAqT8GADUmBQDPCwIAd7j+/z3/+//M1fn/31X2/6Pl9P8x1vT/pF30/1gm9P88A/P/RWLw/0Gq8P+VVvD/ZY/w/wZO8v92KPT/tVb3/yPK9//VJfz/TRP9/y5R//+1wQIAZHoDAMMqBQCm1wUAqBkHALiDBwCkOQkA10YJAA/pCQAyUQkAxHgGAH9yBAAJeQMAALgCAK7eAQALCAAA1en+/0y3//8ydAAANff//zscAAD7kQAAuK///4FdAQCdjgAA/9P//xk8AABZHgAAudgBAGMtAgD5EQMAVH0DAAdFBQDEywYARo8HAFBwCADvFAgATfgJABx9CQCoXQkAz+wJAPteCQCxFQcARmgGAOczCQAvPAoA5AwLAIkzDQAsNg4AZmUQAOYEEgDCFhMAe0QUABRWEgDg+RMACnUUACnSFACWsBMA8mMSAPTEEACj1wwATeAKACtBCADBRgYACIYDAJ73AABG9P//mlD+/wt3/P8r9/v/gRL6/9yW9/9cHfX/65Ty/+RF8P+1h+7/kfrs/zEF7P+A4er/ug7p/7/O6P/9fOj/zEbn/1As5f9XH+X/XdHj/3eE4P+vcOH/ZYPh/1hc4/++o+X/KKPl/1ds5/9jF+j/DqHq/5IT6/928+v/n8br/5eL6/+sE+7/SHfu/4s58P8W5e//WIfw/8bH8v+V5PL/9BHz/9xs8v9k2fD/Aq3w/0oP8P/I8/D/o7Tw//Wh8f8EL/L/O1Tw/3jd8P+lMvD/uq7v/4+D7//NA+3/uDzr/3sG6v+Cdeb/LIjm/93Q5v+w9+b/mpDm/zXQ5f8bEeb/Uifn/wzJ6P+q2uj/ADzp/whH6f/EkOr/iJ/u/z6R8v92W/b/0zX7/7G+/f+a7P7/PhcCAGHuAgBY1AEAODkCAF3KAQBLEAEAb/b+/xMgAQDtkAEASoIBAC5CAwDqswMAdLAGANBiCAAA8AgAVS8LAAMBCwDpWwoAC/0JAKkiCQAOiQoAzw4KACbdCgCM7QsA6cMLANZMDQAvsA4A46gMAOCoDQDaLw0Al5AMAGitCwBviwkAs3UKAInNCgD/OgkAvVgIADRKBgDSnwUAOCQFAFbWAQCC8AMAK/8BAJlyAgCebQIAjNQAAHW2AgDL5wEAbHoCAFtTAwBCNwQAjTcGAO4gBwCQZAgAqnoIALi2CADcAgoAy54JALreBwAvAQYADy8DAL6kAABMlf//1/j7/wTq+v+tYfr/keD5/7UD+//uc/z/r2T+/3AHAADOewEAJ9EDAO/hBQD0EwcAxi0JALMJCgCXmAsA67sMAHhADADUKw0At8QMAMqoDQASnw0A/cwMAOamDAAC+AkASJ8IADNEBwCfnwMAeyABAFNj/f+rg/n/UBD3/yLZ8/90sfT/AQ30//s68//RlvL/pory/wPO8v/3RfP/0H3z/50q8v8NFPD/fp7u/7ms7v9rOO7/sRbx/3jK8f8e//L/9Tr1/0YV9v+r3Pj/oS36//9i+/9kVvz/QND8/3FYAABZTQEAwv0FAHc9CgBVNA4AO0kUADpxFQCv+RYAObYZAIXbGQCjsxsArsAdAOymHQDgXyAA1CAfAN4zIADAth4ApaUcAIK+GwC+lBkAp8kYABICFwCZSxYA0jcXANLIGAD87BgAp7gZANtQGQCWRxoAkAcaAPa5GQC/ohoAJTMaAKtnGQAozxgAb58XAMD5FADtdBQAxaYSAMjODgAIXgoAqxsHAJm6BAA/bQIAzy4CACjMAAANIQEA1CQAAATY//8cnQAARKsAAOy0///taf7/s9r+///y/v9TkgEAuCEDAC9ZBgDmhwkA0tkJAHudCwBgnwwAc74MALByCwApjAkAC+kJAHSOCADdbAYAzYIEAMEiAwAAlwMAwpECADKmAQBuhQEApxj//+qK/f8Vtvr/Agz4/+xn+P9mRPf/vkX1/7x38v/9RvL/F2Lw/y5l7//BAe7/WdDq/1gO6P8A3+X/FS3l/8/X4v/W/OH/8czh/xA55P+aA+b/gcXo//cw6/8nkuz/m0js/yNY7P9vjez/suTq//rr7P+EAO7/IE/v/xCA8f8GMPP/0qb1/y8u9v+29fX/Ou33/8Uh9/8iEfj/25n4/+Dh+P/a+fn/bS34/5zg+f/BCPn/XUX4//2A+f+LmPb/9ir3/2tr9v8ipvT/A4P2/03p9v9n5Pn/6HP8//aE/v8/9f//7S///9wo/v9nIvv/fp/4/9AF9/+FUfT/p+Xz/xa88v+h3/H/0x/1/18T9/9dwvn/p4f9/5Y7AAD/fQEAA60CACnnAwBS5gQAAzgHAGkKBwB1WAkA+OALAPvDCwCyvA0AvPkOAFsCEQB2VhMAh2wVAIzLFQA9fxYAiJoWACGBFwBX8hcAnoAYAAflGAAclRUASd0VADR0FQAcVBYArEUYAHpvGACauhoA/LwbABkBHABrBh0AKOAcADm9HADFsxsA+e8aAAvSGgDHKBoAjywaAGZPGgD87hgAM+8YAKhxGAACaxcAI9IWALF8FQCcNRQAVVAQACA1DQDa2QsAsxAKAO3TCACL4ggADEoHAEe3BQDG1wMAagsDAGfFAABFSgAAr2sBAIosAgCStQMAptgBAJaxAgDaPwEAuFL//1uB/v8T4v3/hbn7/wHh9/8fOfb/9P3z/0kH8/9iuPH/yvPv//EK7/9jSu3//7Pq/6I66v86Vuf/O33n/+Hn5/+0J+b/YNjp/ymE6f/Nyer/+sTr/wNF7P+7Ue7/zmfu/5N37/+LsPH/dKHy/yVr8v+PpvH/j/Pu/5or7/8l2uz/d0bt/8J97f/UTOz/J5Dt/2Pq6/9ka+r/oIvq/zA16f/HT+n/BeLo/9DN5f+emOX/GWDi/2Rc4f+kaeD/fRPe//bd3//COOD/nIzi/1xB5f9uiuX/B0ro/+R26v+DUOv/eiTs/6Ul7v9Sz+3/EsXt/+fn6/9dmer/9mnp/wiC5v8+uOb/fgrl/2wY5f+9uub/aofn/+Q76P+obur/wePs/9kd7v9DD+//x3jy/81u8v/avPT/idT2/9WM9/9iCPn/CoD6/yrN/P9Qtfv/IsH9/1Ma///XiPz/dK/8/xiY/f/KMv3/9jH9/zDH/P/hW/7/ncP9/yW2/v86y///xo7+/6RZ/v9G2v7/ARj+/ys2//8s5P7/YNL9/z1FAADnKgEAcV0FAGXeBwCbgQgApnkLAGLFDADyAA8AqH0OAPgLDACwxQwAuGUNAPKLDgAtQA4AJUAOAM4WDgCSIg0AqYoOAFZRDwDPsRAAAC0QAB1qDwC0JA8ABSUOAN7IDgD/lQ0A1g4NAA95DQD4YA0AH3YMAPSKDQC5Ag8A0+INAOqQDgC8fhAAHsURAKoPEwCfshMA6AgUAJSwFADRvRUAl8oUAK9UEwBCNxMAWuIQACRvDwDbiwsAJsEIAI9sCABT8wYA5G8HANY+BgBQKgYAfkQHAFzmBgAw/gUABNEEAKuuBABpOwQAjVwBADppAABtsf//DF3+/7qj/v+qOv7/IEkAAEc4AgBy+wEAyTABAJnxAQC3B///79X8/6vY+v8st/j/r2D4/3eO9P9mh/b/gvX3//Pn+P9Owfr/hH39/zUrAAAj3AEAF+AFAH3YBQAEQwcAHwQJANitCgCbewwAFIkMAMobDwA1cxAAyOwPAGNiDwBRgA4AZ+YMAF29DQA0XQ0Aun0NAMPTDwDKsg8AuXIRAGWkEADQTxEAzVgTAO7mEgDVWxUAa1AXAI+gGQAH2RsAvc8bAK+nHQD+XB4Af0AfACNmHgAVpR0AdvAeAFb3HADJXB0AGvAaAMFEGAAGQRcAejoTAPEWEwDD3hIAeUMQAHrPDwBojQ0AtXINAKuzCQD/hgQAHGQDADt8//84nP7/lbD8/2bQ+v+YSfn/42n3/0mG9/893PT/FSbz/75R8v9fMPD/zhTw/4cT7/+yTu7/X1Pt/7607f+gq+7/BRTt/6/77f/cou3/nl3v/wc88f/GYPL/Snbz/6L/8v9Bv/T/ElHz/0lv8P/YcPD/yFXv/yZB7P9kZ+n/Er/l/yuL5f+82+T/cj3k/6q/5f8eB+f/a2Ho/9Kg6P/bouj/u7Lo/1gF6P9Kk+b/d0Xm/4fS4/96pOT/2hHm/0ZC5f9M6eT/4Abl/++W5P8kvOT/9tnj/+AS5P8LR+X/AlHm/4FJ6f9fHOv/vtPv/zKE8P8rUvL/f2fz//0+8/+rgfP/9kHz/wYv8/8oJvH/x1Dw//Cn7/82n/D/TKfw/2lZ8v9NVvP/KR7z/0sA8v+PD/L/scHy/9GV9P/+Yff/Y7r4/+w9+v903/r/FID8/8PZ/P+GQ/7/o7kAADsSAQDudAMA+a0FAFnuBwBwwwoAqB8KAGirCgAV0QsAnH4MAMzpDAAkZA0ASHYOAKWpEAAXMxMAM1oUAOFxFgBOXxgAJF0bAIEtHgB05R4A0FIhAAHtIwBphCQAMs0lAJsAJQBqwyUA89EnAPwfJwAc6ScAT/gmANUWJgAftiQA7xgiANVMIQBJ6R4A7nUcAEHgGgDyrRsA9LEaAKggGACyIhgAcvEVANb/FQCCrhUAxUQWAOkEFwDqiRYA/+EWADYQFgDfQRgA5LYYAFziFgATOhUAJUwTAOjADgCh/wsAbbUIAEHwBQAv2wUA96oDADpiBQBhEwgAb5IIAJdfCQBYJQoA/C4KAHa1CgBZLQoAwTsJAKb4CACNKgcAtpkFAFwGBQBqhwQAu5EDAPYkBAAU1AMApfkCAB9kAgAgMv//Zn/9/yX8+/8B+vn/ciP3//O99P88/vD/Cdfw/8NN8P8Nfu7/gYzv/7p47//NcfD/pGbw/6+Y8P+cePH/qmXz/8FU8v9xSvL/tV3x/2yq8f85EPH/6SPw/wsu7//6eOz/Vszr/6Bc6f9nAef/3+Pk/yLH4v9mmeD/hkLf/3ve3f8bNt7/tQLf/7Ir4P//ld//ALDg/+Ka4v/XVeX/DEHo/3d66v/BG+z/mlvt/6Pt7/+rvvL/BV/1/0Ao+P9cn/n/A3P3/ylt9v+PivT/asz0/6+U8/98KvP/g2jz/1m98/+rDvb/zGr4/1E8/f99xwEAzZYDAFWABAANeQYA6lYHAJm2BwAhrQYAfoIGADvcBQB0DAcAejEHAGuIBwCwzQgAUMMIAOsXCwAa3QsAUVQKAGhVCABY7wkAnfAIAP9nBwCkqwUAK9wCAD0aAQCMdf7/8y3+/zXu/v/j3v///PoAAJlyBACZhAYAVYsIAMQNCgDkvwsARwoNAIgyDAAnOA0A4u4LAHX8CQCz3AgAYM8IANALCABtJwYAe38DAJNwAAB1yP7/j2b+/3yj//8FrgAAUnUCADEwBQAEkgcAf7MGAOb2BwCYNAgAircGALSJBwAbdwQALJgEAHW4AwAH2AIAcFACAODiAACOKAEAmSABAKWvAQDFIAEAelYCAJHpAAAFNgAAocn+/1O//v9MJ///Zcr9//mP/v/s2/z/iej7/yeI+v+wI/n/vz/6/8Cc+f9PXfb/hOj2/8me9f+csfT/ifrz/3JV8v9a9PH/MdHw/3kc8v+Vw/H/2KTx//UJ8f/g/PH/ch7z//av9P+7UvX/xEj2/xjM9//e3vb/2Vz3/0VO9v/aePb/a4z2/5v79P/a8vX/n2P2/+549v+2mvT/zJzy/5bB8f8Su/D/zUDx/6En7/+Le+7/PF3u/x7l7v+mR+//uHLw/3NK8/8gf/P/MNTz/1p+9/8xN/j/g2v6/28Q+//q4Pn/FCv6/6/D9f/sKPX/0rfy/8z28P8ZSe//CnHu/9b87//ncfD/H0Xx/00u8v9IbPT//kf2/5bK9v92K/n/uFX7//+G+/8J1Pz/vaz8/yJc/v8Ipf7/OvoAAHO6AwAqlwUA8CcGALh2BABYSgcAADMIAMi8BwDKOwcAmnEJAA5XCwDWJQwAXZoOAHTuDgCfDBAAMxASAK6pEgD4shIAPOQSACcMEgAhuRAAFi4QAASRDwDblw4Ak/cNAI0eDQBmSg4ACKsPAG6WEAAImhAAb44QAP+yDgACuA0AYfAOABx8DAAuOgwAVlcNALqrDgCTNxAAfZ8OAEJsDgBIYQ0AhoYLAK3DCgBENgkAf/kHACb/BAAdGwIALuoAAIKQ//+apP//IvQAAASQAQBWVwIA6NACAOsFBQA25QQA9DYFACEsBQDUBwUAV+gEAGq7AwD7zQMAnvcCAAtzBAA5JwUA+bUFAAhyBgBeiAcA0NUIAGgfCABWbwcA4MoHAK8zBgC6MQUAhNYCAHZCAgBQ9wEAE0YAABvJAQBPpQMAgp8FALvWBgDCtQcAQfIGAGjsBgA0kgcAZ+QGAL/TBgB+swcALFoJAB6TCAABOgcA5oMHALD9BQBZIgUAiksFAD6rBAAHowEAQB8AAOjr/v+hFf7/xQD9/972/P9+d/7/yyz//0e9AABZJQAATvL//zU7AABuY/7/aQv8/xzY+/90R/n/6DT4/5t69f9jbfP/Rbf0/6UT9//ibfr/e2T9/8Q1AACS4gEAp1UFAM6XBwDoVgsAEK0NANpWEACFShIAuVwSAK37EwD69BMASnwSAFA3EQC6yBAAB+QPAAOiDwD1jg4A8swOAJgvEACD7A4AkBMRAOhMEgDB4BEA6jcRAP4dDwDmdA0AyykKACcPCADCOAYAbikDAJsY///Co/v/mUj4/zOM9/+mlvb/zH31/0qX9f9ScPX/YQn0/2ST8/8OYPT/Shvz/+A78v9MCPP/r13z/2jO8v9iEfT/igjz/9vm9P+W//b/ylv5/5pw+//eKPv/Ax78/084/P8Wj/z/GSv8/zXv+v+Iefn/yTr3/4/z9P9/O/T/X7Tz/35F8v94HfL/4ony/wep8P+H+e//UuDu/4pO7/8W3e7/Eovv/y2D8f+iDvH/EHXy/3OV8v8z0PP//Qb2/0al9v/FDff/8Fr4/0tU+v9sx/n/vcj4///u+P9SOPj//iT2/81U9v9Xzvf/Hrv2/4yN9//Po/r/Lp38/9g5/v+zUv//TsP//1VGAQAB/P//fGkBAKIMAQC8r/7/RCP9/xxR+v//nPf/Gxf0/+wN9P9qIPP/1TTz/wio8//mbPP/P6Ly/0EV9P9WKPX/es71/7CK9/+s8/b/7WX2/1lT9v9JU/f/MRD2/0ht9v+nY/X/0DX0//lr8/+FL/P/P87y/70I9P9zs/T/Fzby/28B9P+HA/L/0M3y/xIo9P/4jPX/hBz2//vB9v/DJvn/l1/7/4GG/v+T3/3/KVP//7n9//9rawAA4m8CAPsUAwBC5wMA8UwGAISsBwB0BQkAD6oJAGyyCwDaIwwAarYMABkRDwBo9w8Aw7gRAD5MEQAoDBIADcgRAKeuEQBl0RIAmNsRADOIEgBfhhIAEKgSAFhqFABYShQAu6AUAA== +` diff --git a/integration/context_test.go b/integration/context_test.go index 06163e5ba..463ffd579 100644 --- a/integration/context_test.go +++ b/integration/context_test.go @@ -51,6 +51,7 @@ func TestContextExhaustion(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) defer cancel() // Set up the test data + thinkOff := api.ThinkValue{Value: false} req := api.ChatRequest{ Model: smol, Messages: []api.Message{ @@ -59,6 +60,7 @@ func TestContextExhaustion(t *testing.T) { Content: "Write me a story in english with a lot of emojis", }, }, + Think: &thinkOff, Stream: &stream, Options: map[string]any{ "temperature": 0, diff --git a/integration/llm_image_test.go b/integration/llm_image_test.go index 2c60b9668..5a9eef317 100644 --- a/integration/llm_image_test.go +++ b/integration/llm_image_test.go @@ -15,6 +15,7 @@ func TestVisionModels(t *testing.T) { skipUnderMinVRAM(t, 6) defaultVisionModels := []string{ + "gemma4", "qwen2.5vl", "llama3.2-vision", "gemma3", @@ -23,6 +24,8 @@ func TestVisionModels(t *testing.T) { "ministral-3", } + skipIfNoVisionOverride(t) + for _, model := range testModels(defaultVisionModels) { t.Run(model, func(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) @@ -30,10 +33,7 @@ func TestVisionModels(t *testing.T) { client, _, cleanup := InitServerConnection(ctx, t) defer cleanup() - if testModel != "" { - requireCapability(ctx, t, client, model, "vision") - } - + requireCapability(ctx, t, client, model, "vision") pullOrSkip(ctx, t, client, model) image, err := base64.StdEncoding.DecodeString(imageEncoding) diff --git a/integration/thinking_test.go b/integration/thinking_test.go new file mode 100644 index 000000000..15c8b0740 --- /dev/null +++ b/integration/thinking_test.go @@ -0,0 +1,155 @@ +//go:build integration + +package integration + +import ( + "context" + "strings" + "testing" + "time" + + "github.com/ollama/ollama/api" +) + +// TestThinkingEnabled verifies that when thinking is requested, the model +// produces both thinking and content output without leaking raw channel tags. +func TestThinkingEnabled(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + defer cancel() + + client, _, cleanup := InitServerConnection(ctx, t) + defer cleanup() + + models := testModels([]string{smol}) + for _, modelName := range models { + t.Run(modelName, func(t *testing.T) { + requireCapability(ctx, t, client, modelName, "thinking") + pullOrSkip(ctx, t, client, modelName) + + think := api.ThinkValue{Value: true} + stream := false + req := api.ChatRequest{ + Model: modelName, + Stream: &stream, + Think: &think, + Messages: []api.Message{ + {Role: "user", Content: "What is 12 * 15? Think step by step."}, + }, + Options: map[string]any{ + "temperature": 0, + "seed": 42, + "num_predict": 512, + }, + } + + var response api.ChatResponse + err := client.Chat(ctx, &req, func(cr api.ChatResponse) error { + response = cr + return nil + }) + if err != nil { + if strings.Contains(err.Error(), "model requires more system memory") { + t.Skip("model too large for test system") + } + t.Fatalf("chat failed: %v", err) + } + + content := response.Message.Content + thinking := response.Message.Thinking + + // Thinking should be non-empty when thinking is enabled + if thinking == "" { + t.Error("expected non-empty thinking output when thinking is enabled") + } + + // The answer (180) should appear in thinking, content, or both. + // Some models put everything in thinking and leave content empty + // if they hit the token limit while still thinking. + combined := thinking + " " + content + if !strings.Contains(combined, "180") { + t.Errorf("expected '180' in thinking or content, got thinking=%q content=%q", thinking, content) + } + + // Neither thinking nor content should contain raw channel tags + if strings.Contains(content, "<|channel>") || strings.Contains(content, "") { + t.Errorf("content contains raw channel tags: %s", content) + } + if strings.Contains(thinking, "<|channel>") || strings.Contains(thinking, "") { + t.Errorf("thinking contains raw channel tags: %s", thinking) + } + + t.Logf("thinking (%d chars): %.100s...", len(thinking), thinking) + t.Logf("content (%d chars): %s", len(content), content) + }) + } +} + +// TestThinkingSuppressed verifies that when thinking is NOT requested, +// the model does not leak thinking/channel content into the response. +func TestThinkingSuppressed(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + defer cancel() + + client, _, cleanup := InitServerConnection(ctx, t) + defer cleanup() + + models := testModels([]string{smol}) + for _, modelName := range models { + t.Run(modelName, func(t *testing.T) { + requireCapability(ctx, t, client, modelName, "thinking") + pullOrSkip(ctx, t, client, modelName) + + stream := false + req := api.ChatRequest{ + Model: modelName, + Stream: &stream, + // Think is nil — thinking not requested + Messages: []api.Message{ + {Role: "user", Content: "What is the capital of Japan? Answer in one word."}, + }, + Options: map[string]any{ + "temperature": 0, + "seed": 42, + "num_predict": 64, + }, + } + + var response api.ChatResponse + err := client.Chat(ctx, &req, func(cr api.ChatResponse) error { + response = cr + return nil + }) + if err != nil { + if strings.Contains(err.Error(), "model requires more system memory") { + t.Skip("model too large for test system") + } + t.Fatalf("chat failed: %v", err) + } + + content := response.Message.Content + thinking := response.Message.Thinking + + // The answer should appear in content or thinking + combined := content + " " + thinking + if !strings.Contains(combined, "Tokyo") { + t.Errorf("expected 'Tokyo' in content or thinking, got content=%q thinking=%q", content, thinking) + } + + // Content must NOT contain channel/thinking tags + if strings.Contains(content, "<|channel>") || strings.Contains(content, "") { + t.Errorf("content contains leaked channel tags when thinking not requested: %s", content) + } + if strings.Contains(content, "thought") && strings.Contains(content, "") { + t.Errorf("content contains leaked thinking block: %s", content) + } + + // Thinking field should ideally be empty when not requested. + // Some small models may still produce thinking output; log but don't fail. + if thinking != "" { + t.Logf("WARNING: model produced thinking output when not requested (%d chars): %.100s...", len(thinking), thinking) + } + + t.Logf("content: %s", content) + }) + } +} diff --git a/integration/tools_test.go b/integration/tools_test.go index 9a03fb656..e7e72cf14 100644 --- a/integration/tools_test.go +++ b/integration/tools_test.go @@ -30,6 +30,7 @@ func TestAPIToolCalling(t *testing.T) { defer cleanup() minVRAM := map[string]uint64{ + "gemma4": 8, "qwen3-vl": 16, "gpt-oss:20b": 16, "gpt-oss:120b": 70, diff --git a/integration/utils_test.go b/integration/utils_test.go index fdd62bfcb..f4de4350c 100644 --- a/integration/utils_test.go +++ b/integration/utils_test.go @@ -45,6 +45,7 @@ var ( // Note: add newer models at the top of the list to test them first ollamaEngineChatModels = []string{ + "gemma4", "lfm2.5-thinking", "ministral-3", "qwen3-coder:30b", @@ -137,6 +138,7 @@ var ( "gemma2", "gemma3", "gemma3n", + "gemma4", "glm4", "goliath", "gpt-oss:20b", @@ -272,6 +274,7 @@ var ( "snowflake-arctic-embed2", } libraryToolsModels = []string{ + "gemma4", "lfm2.5-thinking", "qwen3-vl", "gpt-oss:20b", diff --git a/integration/vision_test.go b/integration/vision_test.go index 272296149..aa8b12f87 100644 --- a/integration/vision_test.go +++ b/integration/vision_test.go @@ -5,23 +5,26 @@ package integration import ( "context" "encoding/base64" + "slices" "testing" "time" "github.com/ollama/ollama/api" + "github.com/ollama/ollama/types/model" ) // Default set of vision models to test. When OLLAMA_TEST_MODEL is set, // only that model is tested (with a capability check for vision). var defaultVisionModels = []string{ + "gemma4", "gemma3", "llama3.2-vision", "qwen2.5vl", "qwen3-vl:8b", } -// decodeTestImages returns the two test images (Abbey Road llamas, docs llamas). -func decodeTestImages(t *testing.T) (abbeyRoad, docs api.ImageData) { +// decodeTestImages returns the test images. +func decodeTestImages(t *testing.T) (abbeyRoad, docs, ollamaHome api.ImageData) { t.Helper() var err error abbeyRoad, err = base64.StdEncoding.DecodeString(imageEncoding) @@ -32,9 +35,35 @@ func decodeTestImages(t *testing.T) (abbeyRoad, docs api.ImageData) { if err != nil { t.Fatalf("decode docs image: %v", err) } + ollamaHome, err = base64.StdEncoding.DecodeString(imageEncodingOllamaHome) + if err != nil { + t.Fatalf("decode ollama home image: %v", err) + } return } +// skipIfNoVisionOverride skips the entire test (at parent level) when +// OLLAMA_TEST_MODEL is set to a non-vision model. This prevents the parent +// test from reporting PASS when all subtests are skipped. +func skipIfNoVisionOverride(t *testing.T) { + t.Helper() + if testModel == "" { + return + } + // Check actual model capabilities via the API rather than a hardcoded list. + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + client, _, cleanup := InitServerConnection(ctx, t) + defer cleanup() + resp, err := client.Show(ctx, &api.ShowRequest{Name: testModel}) + if err != nil { + return // let the test proceed and fail naturally + } + if len(resp.Capabilities) > 0 && !slices.Contains(resp.Capabilities, model.CapabilityVision) { + t.Skipf("model override %q does not have vision capability (has %v)", testModel, resp.Capabilities) + } +} + // setupVisionModel pulls the model, preloads it, and skips if not GPU-loaded. func setupVisionModel(ctx context.Context, t *testing.T, client *api.Client, model string) { t.Helper() @@ -54,6 +83,7 @@ func setupVisionModel(ctx context.Context, t *testing.T, client *api.Client, mod // handles cached image tokens across turns. func TestVisionMultiTurn(t *testing.T) { skipUnderMinVRAM(t, 6) + skipIfNoVisionOverride(t) // Models that fail on multi-turn detail questions (e.g. misidentifying objects). skipModels := map[string]string{ @@ -72,7 +102,7 @@ func TestVisionMultiTurn(t *testing.T) { defer cleanup() setupVisionModel(ctx, t, client, model) - abbeyRoad, _ := decodeTestImages(t) + abbeyRoad, _, _ := decodeTestImages(t) // Turn 1: describe the image req := api.ChatRequest{ @@ -100,7 +130,7 @@ func TestVisionMultiTurn(t *testing.T) { api.Message{Role: "user", Content: "How many animals are in the image?"}, ) resp2 := DoChat(ctx, t, client, req, []string{ - "four", "4", + "four", "4", "three", "3", }, 60*time.Second, 30*time.Second) if resp2 == nil { t.Fatal("no response from turn 2") @@ -121,6 +151,7 @@ func TestVisionMultiTurn(t *testing.T) { // TestVisionObjectCounting asks the model to count objects in an image. func TestVisionObjectCounting(t *testing.T) { skipUnderMinVRAM(t, 6) + skipIfNoVisionOverride(t) skipModels := map[string]string{ "llama3.2-vision": "consistently miscounts (says 3 instead of 4)", @@ -137,7 +168,7 @@ func TestVisionObjectCounting(t *testing.T) { defer cleanup() setupVisionModel(ctx, t, client, model) - _, docs := decodeTestImages(t) + _, docs, _ := decodeTestImages(t) req := api.ChatRequest{ Model: model, @@ -160,6 +191,7 @@ func TestVisionObjectCounting(t *testing.T) { // cultural references and scene context from an image. func TestVisionSceneUnderstanding(t *testing.T) { skipUnderMinVRAM(t, 6) + skipIfNoVisionOverride(t) // Models known to be too small or not capable enough for cultural reference detection. skipModels := map[string]string{ @@ -178,7 +210,7 @@ func TestVisionSceneUnderstanding(t *testing.T) { defer cleanup() setupVisionModel(ctx, t, client, model) - abbeyRoad, _ := decodeTestImages(t) + abbeyRoad, _, _ := decodeTestImages(t) req := api.ChatRequest{ Model: model, @@ -193,7 +225,7 @@ func TestVisionSceneUnderstanding(t *testing.T) { Options: map[string]any{"temperature": 0.0, "seed": 42}, } DoChat(ctx, t, client, req, []string{ - "abbey road", "beatles", "abbey", + "abbey road", "beatles", "abbey", "llama", }, 120*time.Second, 30*time.Second) }) } @@ -203,6 +235,7 @@ func TestVisionSceneUnderstanding(t *testing.T) { // objects based on their spatial position in the image. func TestVisionSpatialReasoning(t *testing.T) { skipUnderMinVRAM(t, 6) + skipIfNoVisionOverride(t) for _, model := range testModels(defaultVisionModels) { t.Run(model, func(t *testing.T) { @@ -212,7 +245,7 @@ func TestVisionSpatialReasoning(t *testing.T) { defer cleanup() setupVisionModel(ctx, t, client, model) - _, docs := decodeTestImages(t) + _, docs, _ := decodeTestImages(t) // The docs image has: leftmost llama on laptop with glasses, // rightmost llama sleeping. @@ -239,6 +272,7 @@ func TestVisionSpatialReasoning(t *testing.T) { // small details like accessories in an image. func TestVisionDetailRecognition(t *testing.T) { skipUnderMinVRAM(t, 6) + skipIfNoVisionOverride(t) for _, model := range testModels(defaultVisionModels) { t.Run(model, func(t *testing.T) { @@ -248,7 +282,7 @@ func TestVisionDetailRecognition(t *testing.T) { defer cleanup() setupVisionModel(ctx, t, client, model) - _, docs := decodeTestImages(t) + _, docs, _ := decodeTestImages(t) req := api.ChatRequest{ Model: model, @@ -274,6 +308,7 @@ func TestVisionDetailRecognition(t *testing.T) { // encoding and cross-image reasoning. func TestVisionMultiImage(t *testing.T) { skipUnderMinVRAM(t, 6) + skipIfNoVisionOverride(t) // Multi-image support varies across models. skipModels := map[string]string{ @@ -291,7 +326,7 @@ func TestVisionMultiImage(t *testing.T) { defer cleanup() setupVisionModel(ctx, t, client, model) - abbeyRoad, docs := decodeTestImages(t) + abbeyRoad, docs, _ := decodeTestImages(t) req := api.ChatRequest{ Model: model, @@ -314,10 +349,12 @@ func TestVisionMultiImage(t *testing.T) { } } -// TestVisionOCR tests text extraction from an image. The docs image -// contains the text "Ollama's documentation" in a header. -func TestVisionOCR(t *testing.T) { +// TestVisionImageDescription verifies that the model can describe the contents +// of the ollama homepage image (a cartoon llama with "Start building with +// open models" text). Basic sanity check that the vision pipeline works. +func TestVisionImageDescription(t *testing.T) { skipUnderMinVRAM(t, 6) + skipIfNoVisionOverride(t) for _, model := range testModels(defaultVisionModels) { t.Run(model, func(t *testing.T) { @@ -327,22 +364,22 @@ func TestVisionOCR(t *testing.T) { defer cleanup() setupVisionModel(ctx, t, client, model) - _, docs := decodeTestImages(t) + _, _, ollamaHome := decodeTestImages(t) req := api.ChatRequest{ Model: model, Messages: []api.Message{ { Role: "user", - Content: "What text appears in this image? Read all visible text.", - Images: []api.ImageData{docs}, + Content: "Describe what you see in this image briefly.", + Images: []api.ImageData{ollamaHome}, }, }, Stream: &stream, Options: map[string]any{"temperature": 0.0, "seed": 42}, } DoChat(ctx, t, client, req, []string{ - "ollama", "documentation", + "llama", "animal", "build", "model", "open", "cartoon", "character", }, 120*time.Second, 30*time.Second) }) } diff --git a/integration/vision_test_data_test.go b/integration/vision_test_data_test.go index e7d324601..be917d29b 100644 --- a/integration/vision_test_data_test.go +++ b/integration/vision_test_data_test.go @@ -383,3 +383,162 @@ yEUu0pztbKtys2RR9bUiUBGoCFQE5oTAL3/5y+ab3/xmc9JJJzWf+cxnmq9+9atzKXmuDGQuNaqFVAQq VBGoCFQElgKBykCWoptqJSsCFYGKwOIhUBnI4vVJrVFFoCJQEVgKBCoDWYpuqpWsCFQEKgKLh0BlIIvXJ7VGFYGKQEVgKRDYOWr5q6Woaa1kRaAiUBGoCCwU Av8fgwPy24mbuF8AAAAASUVORK5CYII= ` +// imageEncodingOllamaHome is a 415x293 JPEG of the ollama.com homepage. +// Shows a cartoon llama character with text "Start building with open models". +const imageEncodingOllamaHome = `/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAA0JCgsKCA0LCgsODg0PEyAVExISEyccHhcgLikxMC4pLSwzOko+MzZGNywtQFdBRkxO +UlNSMj5aYVpQYEpRUk//2wBDAQ4ODhMREyYVFSZPNS01T09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09P +T09PT09PT0//wAARCAElAZ8DASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUF +BAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVW +V1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi +4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAEC +AxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVm +Z2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq +8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD06iiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiq +2o39rpllLeXsqxQRDLMf5e5oAs0V5XffEXXL6WeXQdOC2dsNzu8ZchfVuwrufCOvDxFocd80YjlDFJUHQMPT270AbdFFFABRRRQA +UUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUU +AFFFFABRRRQAUUUUAFeUeI7u68b+L00PT5CLG2chnHTj7zn+Q/8Ar12XjTxLZ6LpNzD9pUX8sRWGIcsCRjJ9BXmPg/xPJ4djuDa6 +V9rnnI3SliMKO3A9eaAO/wDFyWHhfwDPYWSLGJlECDu5PUn1OM1V+HuoaVovhWFb7UbWGa4kaUo0oyAeBkduBXMXc2t/EfV44obc +W1vbLyGJKR56knHJPpXT2fwr0mOMfa7y6mkxyVwg/Ac0AdpZ6lY3wzZ3kE//AFzkDfyq1XmupfDF7YfafD2oypcJyqSnBP0YdKzU ++IWuabp1xpV/bltUjPlpM45X13DufQ96APQtf8U6T4fTF9PmYjKwxjc5/Dt+Nc7pnxP0291GO1ns5rZJWCrKzhgCemR2qn4V8Atd +v/a/ikvNPMd4t3Y9+7n19qzfHsVrfeLNM0PSoIkaHCMIkAwWI449AM/jQB61RSKNqgegxS0AFFFFABRRRQAUUUUAFFFFABRRRQAU +UUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFBIAyTgCsifxBbCYw2UU17KvUQLkD8e +lAGvRSIxZFYqVJGSD2paACub8b+JB4c0fzItrXk5KQKegPdj7D/Cukry7xWn9s/FLT9LnOYItgK9iMbz+fSgCfwh4I/tEDW/Exe4 +luD5iQyE8g/xP/hXosFtb20Qit4I4kHRUUAD8qkAAAAGBXC6d4pvtL8XXGieIZ45Yp5M2064AXJ4U47duehoA7pUVc7VAzycDrS1 +FPc29uM3E8cQ9XcL/OiG5t7gZt54pR/sOG/lQBLXn/xH8OXt3dWesaNbNLdQnEojGWOOVOO+OlegUUAeXr8ULuCxuLfUNMMeoouI +yMhd3+0p5HrV34ceHbgzSeJNWDNc3GTCH64PVz9e3tW94z8LW3iDTJGWNVv4lJhlA5J/un1BrK+F2tzXumTaXeMTPYkBN3XYeMfg +ePyoA7qiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKK +KACiimyIJInjJIDKRkdRmgDAmeTXrmWJZTDpduSJXBwZiOoz6ClsLq4uGWLQ7OGGwjbBmkBG/wBdoHX61dk0dP7DOl20rRJtC78Z +JGcnP1qle+IbDSsWNvG0hiXZ8mAF9s+tAHK6/wCKdc1nxDJofhTKCElXmXGWI6nJ+6oPFVk8ReKvCGoxReJQ13Zyn7+Q3Hcqw7j0 +NSfCJkbUdXZv9aQpyeuMnP613+u6Pa65pcthdr8rjKt3RuzCgC1Z3UF7aRXVrIJIZVDIw7g15p468zQvH2na8ULQPtLY9V4Yf98k +VF4X1u58FaxLoGvZW0LZSTshP8Q/2T+n51FbxXHxF8XySTM6aTaHgA9FzwB/tNjJNAHpd9Aut6G8VpevCl1GCk8J5APORXC63oWk +eCdIW/hha+1SSQJBLcfMFfruC9OP8K9Gt4Ira3jggRY4o1Coq9FA6CuT+Jem3F74fS6tFLS2Mon2gZyuOfy60AYkfhCxAt7rxnqs +0t/ek7IzLtXdjO3d/wDqFY+k6XpOrXwttEfUtK1VC/RvMiUr0y4wRmug8RahB4s8F295ZR/aHtpo5Lq3UZkUDhgO/wCPpVXQtK/t +PxBJP4b/ALQ0jRgi+a24r5zjoFB/Xr+tAGx4V8SajHqz+HPEqhb9B+5m7TD+vHQ96u6z480PSpXt/Oe6uUO0xQLuwfQnpWN43eKb +xx4dgsyDfRygvt6qm4EZ/JjS6h4avtE8XQa1oNot1b3MmLi3IB8sk8kE9B3z2+lAHdWdwt3Zw3Ko6LKgcK4wwyM4I9a828G4i+KO +sxQcRHzuB0++K7/XdWg0XSJ7+5YARr8o7u3YD8a4j4U2E0smoa7cg7rhiiEj73OWP54FAHo9FFFABRRRQAUUUUAFFFFABRRRQAUU +UUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFADJiwhcp94KcfXFcl4NgguJLuS4RZ +JRj74zgHOf1rsK4u4EnhvxCZ1Um0nJOB3U9R9QeaAOdsivhL4pSwP+7s70kL2AV+V/JuK9WriviBoS+INCj1LTsSXNqpdCv/AC0T +uPqOtWPh94mXXdJFvcv/AKdagLICeXXs3+PvQA34mWFnP4VuLueBWuLfHkydCuWA/L2pPhdaR2/hCKZQN9xI7ufXBwP0Fb/iHTf7 +Y0K80/IDTRkKT2bqP1ArB+G9lq2m6LNZarbGBYpj5O48kHr+Gen1oA6+ggEYNFFAHF6t4Bie+OoeH76TS7snJEedhP0HT+VVjonj +9h5B8QWwj6eYBhv/AEHNd7RQBzHhfwdb6HO99c3D3uoyZ3Tyds9cf4109FFAHlfxYiu11ewmupXfTGGFjU42sD834kdDXpWlwWtv +pltFYIEtljXygP7uMiuT+LMaN4TV2A3JcptP1Brd8Hu0nhLS2fkm2T+VAGzRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUU +AFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAVWv7GDULVre4XKnoR1U+oqzRQBxSSaj4WuSki+dZ +O3H90/T0PtXK69GNF1mPxP4bfEJfM8GMGJj1BH91v5/hXrsscc0bRyorowwVYZBrl9W8HxTK5sGChgQ0Mn3SPQHtQBpaV4k07UtB +OrrMscEa5mDHmIjqD/nmrOjavY63Yi806XzItxU5UggjsRXiOv6VqXh2WSzJmitrz+DPD4OcH1we9e0eGNKi0bQLSyjXDKgaQ/3n +PJNAGrRRRQAUUUUAFFFFAHmvxW1H7XNY6BaZknaQSOq9ieFH6k13+lWY0/SrWzHSCJY/yFcJ8U9GEKQeIrMmO5idUlZT1/ut9QeP +yrtfD2o/2toNlfnAaeIFsf3uh/UGgDRooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooA +KKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigDzD4rZGuaKW/1fP/oQzXpy4IBHTtXC/FnTHutBgvolJazky+OytwT+YFdH +4T1P+1/DdleEEM0e18/3l4P8qANiiiigAooooArajfW+m2E17dvshhUsx/w968zOveL/ABldSJoKNZ2SHG5Ttx/vP6+wrqfibb3F +x4On+zgt5ciSSAd0B5/ofwqD4d65pEvh610+KaKC6hXbJE5Clmzyw9c0Ac3efD3xPPaO0+sJcSEZ8lpnIb8TxWr8M9ddQ/hq/i8m +4tN3l5GCQD8yn3BP5V6CzKqFmYBQMkk8AV5Xpk0er/F57zTObeMszyL0YBNpP4mgD1WiiigAooooAKKKKACiiigAooooAKKKKACi +iigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAGyIkiMkiK6MMFWGQR9KSKK +OGJYoY1jjUYVVGAPoKfRQAUUUUAFFFFACMoZSrAEEYIPeuJ1r4aaTfyvPYSyWMrHO1BuTP07fga7eigDy9vhtrxUwHX1NueCpaTG +P93pXaeF/DFj4aszFbZknkwZZmHL+3sPatyigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiii +gAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAoo +ooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAK +KKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKA +CiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiii +gAooooAKZPNHbwSTTOEjjUszHoAOSafWR4tt5rrwrqcNsC0rW7bQOp4zj8qAPPdQ8a+IvEeqNY+GIpIoudvlqPMYf3mY8KPy+tJN +p/xIsIzdfabuXb8xVbhZSP8AgPf8Ki+F2vaZpVxeW2oSJbtc7DHM/C8Z+Unt1zXrcM0U8YkgkSRD0ZGBH5igDiPAvjmTWbn+zNWV +EvcExyKNolx1BHZu/wCddJ4n16Pw7pQv5YHmXzFj2owB5zzz9Kz/APhB9LXxH/bkU11Hced52xWUJu78Yzg/XvXH/EvUtdke5sJ7 +DZpUcyGK48phuO3+9nB5J/KgD0Lw3rcfiDSU1CKF4VZ2XYxBPB9q1a8j8Cax4lt4LKysdM83THuQHn8hmwCw3fMDjiu+8W+J7bwz +p6zSoZriUkQwg43EdST2AoA3qK8qi8W+OtRiN5Y6Xm2PK+XallI9iTk/hWz4P8ftq2oLper26W92xKo6AhWYfwkHoaAO8orkvH/i +W+8N2tnLYJAzTOyt5qk8AA8YIrnLjx9r1/b28OhWHnzrCrXMkUDSAOR90DsB70Aej6neLp+mXV66F1t4mlKg4JwM4rF8JeLYPFBu +hDaSQfZtmd7A53Z9PpWB4w1jxGmgW0cOn+ZDdaduvZDA37pivzd/lxz1ri/Bmq+INMN5/YGn/a/M2eb+5aTbjOOh46mgD3aq2oyP +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 +ugqpFsRQqqMAAYAFOoIyCKAPFfhpY22p67f2V7EJYJbJwyn/AH059j710k3wntGuC0GqzRw54RogzD8cj+VbPhXwPD4b1SS+jv5L +gyRGLa0YXGSDnr7V1tAGP4c8Nad4ctmisUYySY8yaQ5d/wDAe1XtS0601Wyks7+FZoJByp/mD2PvVqigDzm6+E9m8pa01WaKMnhZ +Ig5H45FbnhvwHpWg3C3eXu7tfuySgAJ7qo6H35rqqKAMfxRoEXiPTEsZp3hVZRJuQAngEY5+tS+HtHj0HR4dOileVIixDsACcknt +9a06KAMjxNoUXiLSxYTTvCvmCTcgBPGfX60/w5osegaRHp8MzzIjMwZwAeTntWpRQAVykngi2fxYNfN7MJfOEvlbRtyB0z1rq6KA +CuV0LwTbaLrsmqxXs0ruHBRlAHzHPauqooAhvbcXdjcWrMVE0bRlh1GRjP61geE/CFv4YluZILuWc3CqCHUDGM+n1rpaKAMjxD4b +03xFbrFqER3p/q5UOHT6H09jXG/8Klt/Nz/bEvl/3fIGfzz/AEr0migDH8O+GdM8OwNHYRkyP/rJpDl3/HsPYVf1GwtdTspLO+hW +WCUYZT/Meh96s0UAec3Hwns2nLW2qzRxE/ceIOQPrkfyrUl+HWlf2ENMglmiJlWWSfAZ5CAQAewHzHgV2VFAGb4f0iPQtGh02KVp +Uh3YdgATlie31rI8SeBdK1+c3TF7W7b70sQGH/3gev1rqaKAPObb4T2iTBrnVZpY8/dSIISPqSf5V3emabZ6TYpZ2EKwwp0A7n1J +7n3q3RQByg8EWw8V/wBv/bZvN87zfK2jbnHTPWtrW9EsNdsTaajFvTOVYHDIfUHtWjRQB5vJ8J4PMPk6zMkZ/haEE4+oI/lXSeGf +Bel+HXM8Iee7Ix50uMqO4UDgfzrpKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiig +AooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooo +oAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKK +KKACiiigAooooAKKKKACiiigAooooAKKKKAP/9k=` diff --git a/llama/patches/0035-CUDA-get_rows-q6_k-support.patch b/llama/patches/0035-CUDA-get_rows-q6_k-support.patch new file mode 100644 index 000000000..a6ba891c1 --- /dev/null +++ b/llama/patches/0035-CUDA-get_rows-q6_k-support.patch @@ -0,0 +1,121 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Daniel Hiltgen +Date: Fri, 20 Mar 2026 18:50:38 -0700 +Subject: [PATCH] CUDA get_rows q6_k support + +--- + ggml/src/ggml-cuda/getrows.cu | 80 ++++++++++++++++++++++++++++++++- + ggml/src/ggml-cuda/ggml-cuda.cu | 1 + + 2 files changed, 80 insertions(+), 1 deletion(-) + +diff --git a/ggml/src/ggml-cuda/getrows.cu b/ggml/src/ggml-cuda/getrows.cu +index 2fab33243..dc5c4f57a 100644 +--- a/ggml/src/ggml-cuda/getrows.cu ++++ b/ggml/src/ggml-cuda/getrows.cu +@@ -155,6 +155,81 @@ static void get_rows_cuda_float( + s10, s11, s12/*, s13*/); + } + ++// Specialized GET_ROWS kernel for Q6_K — the k_get_rows template doesn't work for K-quants ++// because they lack the simple dequantize_kernel_t (float2) interface. ++// Based on dequantize_block_q6_K from convert.cu with row-selection logic added. ++template ++static __global__ void k_get_rows_q6_K( ++ const void * __restrict__ src0, const int32_t * __restrict__ src1, dst_t * __restrict__ dst, ++ const int64_t ne00, ++ const int64_t ne11, const int64_t ne12, ++ const size_t s1, const size_t s2, const size_t s3, ++ const size_t nb01, const size_t nb02, const size_t nb03, ++ const size_t s10, const size_t s11, const size_t s12) { ++ ++ const int64_t i10 = blockIdx.x; // row index into src1 ++ const int64_t z = blockIdx.z; ++ const int64_t i11 = z / ne12; ++ const int64_t i12 = z % ne12; ++ ++ const int i01 = src1[i10*s10 + i11*s11 + i12*s12]; ++ ++ dst_t * dst_row = dst + i10*s1 + i11*s2 + i12*s3; ++ const char * src0_row = (const char *)src0 + i01*nb01 + i11*nb02 + i12*nb03; ++ ++ const int64_t nb = ne00 / QK_K; // number of Q6_K blocks per row ++ ++ // blockIdx.y iterates over Q6_K blocks within the row ++ for (int64_t iblk = blockIdx.y; iblk < nb; iblk += gridDim.y) { ++ const block_q6_K * x = (const block_q6_K *)src0_row + iblk; ++ ++ // Same dequantization as dequantize_block_q6_K (assumes 64 threads) ++ const int64_t tid = threadIdx.x; ++ const int64_t ip = tid / 32; // 0 or 1 ++ const int64_t il = tid - 32*ip; // 0..31 ++ const int64_t is = 8*ip + il/16; ++ ++ const int64_t y_offset = iblk * QK_K + 128*ip + il; ++ ++ const float d = x->d; ++ const uint8_t * ql = x->ql + 64*ip + il; ++ const uint8_t qh = x->qh[32*ip + il]; ++ const int8_t * sc = x->scales + is; ++ ++ if (y_offset + 0 < ne00) dst_row[y_offset + 0] = ggml_cuda_cast(d * sc[0] * ((int8_t)((ql[ 0] & 0xF) | (((qh >> 0) & 3) << 4)) - 32)); ++ if (y_offset + 32 < ne00) dst_row[y_offset + 32] = ggml_cuda_cast(d * sc[2] * ((int8_t)((ql[32] & 0xF) | (((qh >> 2) & 3) << 4)) - 32)); ++ if (y_offset + 64 < ne00) dst_row[y_offset + 64] = ggml_cuda_cast(d * sc[4] * ((int8_t)((ql[ 0] >> 4) | (((qh >> 4) & 3) << 4)) - 32)); ++ if (y_offset + 96 < ne00) dst_row[y_offset + 96] = ggml_cuda_cast(d * sc[6] * ((int8_t)((ql[32] >> 4) | (((qh >> 6) & 3) << 4)) - 32)); ++ } ++} ++ ++template ++static void get_rows_cuda_q6_K( ++ const void * src0_d, const int32_t * src1_d, dst_t * dst_d, ++ const int64_t ne00, const size_t nb01, const size_t nb02, const size_t nb03, ++ const int64_t ne10, const int64_t ne11, const int64_t ne12, const size_t nb10, const size_t nb11, const size_t nb12, ++ const size_t nb1, const size_t nb2, const size_t nb3, ++ cudaStream_t stream) { ++ const int64_t nb_blocks = ne00 / QK_K; ++ const dim3 block_dims(64, 1, 1); ++ const dim3 block_nums(ne10, MIN(nb_blocks, (int64_t)UINT16_MAX), MIN(ne11*ne12, (int64_t)UINT16_MAX)); ++ ++ const size_t s1 = nb1 / sizeof(dst_t); ++ const size_t s2 = nb2 / sizeof(dst_t); ++ const size_t s3 = nb3 / sizeof(dst_t); ++ ++ const size_t s10 = nb10 / sizeof(int32_t); ++ const size_t s11 = nb11 / sizeof(int32_t); ++ const size_t s12 = nb12 / sizeof(int32_t); ++ ++ k_get_rows_q6_K<<>>( ++ src0_d, src1_d, dst_d, ++ ne00, ne11, ne12, ++ s1, s2, s3, ++ nb01, nb02, nb03, ++ s10, s11, s12); ++} ++ + template + static void ggml_cuda_get_rows_switch_src0_type( + const void * src0_d, const ggml_type src0_type, const int32_t * src1_d, dst_t * dst_d, +@@ -199,8 +274,11 @@ static void ggml_cuda_get_rows_switch_src0_type( + get_rows_cuda_q(src0_d, src1_d, dst_d, + ne00, nb01, nb02, nb03, ne10, ne11, ne12, nb10, nb11, nb12, nb1, nb2, nb3, stream); + break; ++ case GGML_TYPE_Q6_K: ++ get_rows_cuda_q6_K(src0_d, src1_d, dst_d, ++ ne00, nb01, nb02, nb03, ne10, ne11, ne12, nb10, nb11, nb12, nb1, nb2, nb3, stream); ++ break; + default: +- // TODO: k-quants + GGML_ABORT("%s: unsupported src0 type: %s\n", __func__, ggml_type_name(src0_type)); + break; + } +diff --git a/ggml/src/ggml-cuda/ggml-cuda.cu b/ggml/src/ggml-cuda/ggml-cuda.cu +index 5c9dfd032..b8ed3709b 100644 +--- a/ggml/src/ggml-cuda/ggml-cuda.cu ++++ b/ggml/src/ggml-cuda/ggml-cuda.cu +@@ -4693,6 +4693,7 @@ static bool ggml_backend_cuda_device_supports_op(ggml_backend_dev_t dev, const g + case GGML_TYPE_Q5_0: + case GGML_TYPE_Q5_1: + case GGML_TYPE_Q8_0: ++ case GGML_TYPE_Q6_K: + return true; + default: + return false; diff --git a/middleware/openai.go b/middleware/openai.go index 76853aca5..1f9eb080e 100644 --- a/middleware/openai.go +++ b/middleware/openai.go @@ -678,3 +678,113 @@ func ImageEditsMiddleware() gin.HandlerFunc { c.Next() } } + +// TranscriptionWriter collects streamed chat responses and outputs a transcription response. +type TranscriptionWriter struct { + BaseWriter + responseFormat string + text strings.Builder +} + +func (w *TranscriptionWriter) Write(data []byte) (int, error) { + code := w.ResponseWriter.Status() + if code != http.StatusOK { + return w.writeError(data) + } + + var chatResponse api.ChatResponse + if err := json.Unmarshal(data, &chatResponse); err != nil { + return 0, err + } + + w.text.WriteString(chatResponse.Message.Content) + + if chatResponse.Done { + text := strings.TrimSpace(w.text.String()) + + if w.responseFormat == "text" { + w.ResponseWriter.Header().Set("Content-Type", "text/plain") + _, err := w.ResponseWriter.Write([]byte(text)) + if err != nil { + return 0, err + } + return len(data), nil + } + + w.ResponseWriter.Header().Set("Content-Type", "application/json") + resp := openai.TranscriptionResponse{Text: text} + if err := json.NewEncoder(w.ResponseWriter).Encode(resp); err != nil { + return 0, err + } + } + + return len(data), nil +} + +// TranscriptionMiddleware handles /v1/audio/transcriptions requests. +// It accepts multipart/form-data with an audio file and converts it to a chat request. +func TranscriptionMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + // Parse multipart form (limit 25MB). + if err := c.Request.ParseMultipartForm(25 << 20); err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, openai.NewError(http.StatusBadRequest, "failed to parse multipart form: "+err.Error())) + return + } + + model := c.Request.FormValue("model") + if model == "" { + c.AbortWithStatusJSON(http.StatusBadRequest, openai.NewError(http.StatusBadRequest, "model is required")) + return + } + + file, _, err := c.Request.FormFile("file") + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, openai.NewError(http.StatusBadRequest, "file is required: "+err.Error())) + return + } + defer file.Close() + + audioData, err := io.ReadAll(file) + if err != nil { + c.AbortWithStatusJSON(http.StatusInternalServerError, openai.NewError(http.StatusInternalServerError, "failed to read audio file")) + return + } + + if len(audioData) == 0 { + c.AbortWithStatusJSON(http.StatusBadRequest, openai.NewError(http.StatusBadRequest, "audio file is empty")) + return + } + + req := openai.TranscriptionRequest{ + Model: model, + AudioData: audioData, + ResponseFormat: c.Request.FormValue("response_format"), + Language: c.Request.FormValue("language"), + Prompt: c.Request.FormValue("prompt"), + } + + chatReq, err := openai.FromTranscriptionRequest(req) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, openai.NewError(http.StatusBadRequest, err.Error())) + return + } + + var b bytes.Buffer + if err := json.NewEncoder(&b).Encode(chatReq); err != nil { + c.AbortWithStatusJSON(http.StatusInternalServerError, openai.NewError(http.StatusInternalServerError, err.Error())) + return + } + + c.Request.Body = io.NopCloser(&b) + c.Request.ContentLength = int64(b.Len()) + c.Request.Header.Set("Content-Type", "application/json") + + w := &TranscriptionWriter{ + BaseWriter: BaseWriter{ResponseWriter: c.Writer}, + responseFormat: req.ResponseFormat, + } + + c.Writer = w + c.Next() + } +} diff --git a/ml/backend.go b/ml/backend.go index 3d8f85e5c..37efd365e 100644 --- a/ml/backend.go +++ b/ml/backend.go @@ -137,6 +137,7 @@ type Tensor interface { Bytes() []byte Floats() []float32 + BackendGet() []float32 FromBytes([]byte) FromFloats([]float32) @@ -162,6 +163,7 @@ type Tensor interface { AvgPool2D(ctx Context, k, s int, p float32) Tensor Conv2D(ctx Context, weight Tensor, s0, s1, p0, p1, d0, d1 int) Tensor Conv3D(ctx Context, weight Tensor, c, s0, s1, s2, p0, p1, p2, d0, d1, d2 int) Tensor + Conv1DDW(ctx Context, weight Tensor, s, p, d int) Tensor SSMConv(ctx Context, kernel Tensor) Tensor SSMScan(ctx Context, x, dt, A, B, C, ids Tensor) Tensor @@ -187,6 +189,9 @@ type Tensor interface { Contiguous(ctx Context, shape ...int) Tensor Pad(ctx Context, shape ...int) Tensor + // PadExt pads with independent left/right amounts per dimension. + // Arguments: lp0, rp0, lp1, rp1, lp2, rp2, lp3, rp3 for dims 0-3. + PadExt(ctx Context, lp0, rp0, lp1, rp1, lp2, rp2, lp3, rp3 int) Tensor Stack(ctx Context, dim int, s ...Tensor) Tensor diff --git a/ml/backend/ggml/ggml.go b/ml/backend/ggml/ggml.go index c2ce30dd8..8ec8c94f9 100644 --- a/ml/backend/ggml/ggml.go +++ b/ml/backend/ggml/ggml.go @@ -1069,6 +1069,21 @@ func (t *Tensor) Floats() (data []float32) { return } +func (t *Tensor) BackendGet() []float32 { + n := int(C.ggml_nelements(t.t)) + if n == 0 { + return nil + } + + if t.sync != nil { + t.sync() + } + + data := make([]float32, n) + C.ggml_backend_tensor_get(t.t, unsafe.Pointer(&data[0]), 0, C.ggml_nbytes(t.t)) + return data +} + func tensorSet[S ~[]E, E byte | float32 | int32](t *Tensor, s S) { if len(s) == 0 { return @@ -1313,6 +1328,13 @@ func (t *Tensor) Pad(ctx ml.Context, shape ...int) ml.Tensor { } } +func (t *Tensor) PadExt(ctx ml.Context, lp0, rp0, lp1, rp1, lp2, rp2, lp3, rp3 int) ml.Tensor { + return &Tensor{ + b: t.b, + t: C.ggml_pad_ext(ctx.(*Context).ctx, t.t, C.int(lp0), C.int(rp0), C.int(lp1), C.int(rp1), C.int(lp2), C.int(rp2), C.int(lp3), C.int(rp3)), + } +} + // Permute permutes t according to order. Permute panics if the number of dimensions // in order does not match the number of dimensions in t. func (t *Tensor) Permute(ctx ml.Context, order ...int) ml.Tensor { @@ -1660,6 +1682,13 @@ func (t *Tensor) Conv2D(ctx ml.Context, t2 ml.Tensor, s0, s1, p0, p1, d0, d1 int } } +func (t *Tensor) Conv1DDW(ctx ml.Context, weight ml.Tensor, s, p, d int) ml.Tensor { + return &Tensor{ + b: t.b, + t: C.ggml_conv_1d_dw(ctx.(*Context).ctx, weight.(*Tensor).t, t.t, C.int(s), C.int(p), C.int(d)), + } +} + func (t *Tensor) Conv3D(ctx ml.Context, t2 ml.Tensor, c, s0, s1, s2, p0, p1, p2, d0, d1, d2 int) ml.Tensor { var tt ml.Tensor = &Tensor{ b: t.b, diff --git a/ml/backend/ggml/ggml/src/ggml-cuda/getrows.cu b/ml/backend/ggml/ggml/src/ggml-cuda/getrows.cu index 2fab33243..dc5c4f57a 100644 --- a/ml/backend/ggml/ggml/src/ggml-cuda/getrows.cu +++ b/ml/backend/ggml/ggml/src/ggml-cuda/getrows.cu @@ -155,6 +155,81 @@ static void get_rows_cuda_float( s10, s11, s12/*, s13*/); } +// Specialized GET_ROWS kernel for Q6_K — the k_get_rows template doesn't work for K-quants +// because they lack the simple dequantize_kernel_t (float2) interface. +// Based on dequantize_block_q6_K from convert.cu with row-selection logic added. +template +static __global__ void k_get_rows_q6_K( + const void * __restrict__ src0, const int32_t * __restrict__ src1, dst_t * __restrict__ dst, + const int64_t ne00, + const int64_t ne11, const int64_t ne12, + const size_t s1, const size_t s2, const size_t s3, + const size_t nb01, const size_t nb02, const size_t nb03, + const size_t s10, const size_t s11, const size_t s12) { + + const int64_t i10 = blockIdx.x; // row index into src1 + const int64_t z = blockIdx.z; + const int64_t i11 = z / ne12; + const int64_t i12 = z % ne12; + + const int i01 = src1[i10*s10 + i11*s11 + i12*s12]; + + dst_t * dst_row = dst + i10*s1 + i11*s2 + i12*s3; + const char * src0_row = (const char *)src0 + i01*nb01 + i11*nb02 + i12*nb03; + + const int64_t nb = ne00 / QK_K; // number of Q6_K blocks per row + + // blockIdx.y iterates over Q6_K blocks within the row + for (int64_t iblk = blockIdx.y; iblk < nb; iblk += gridDim.y) { + const block_q6_K * x = (const block_q6_K *)src0_row + iblk; + + // Same dequantization as dequantize_block_q6_K (assumes 64 threads) + const int64_t tid = threadIdx.x; + const int64_t ip = tid / 32; // 0 or 1 + const int64_t il = tid - 32*ip; // 0..31 + const int64_t is = 8*ip + il/16; + + const int64_t y_offset = iblk * QK_K + 128*ip + il; + + const float d = x->d; + const uint8_t * ql = x->ql + 64*ip + il; + const uint8_t qh = x->qh[32*ip + il]; + const int8_t * sc = x->scales + is; + + if (y_offset + 0 < ne00) dst_row[y_offset + 0] = ggml_cuda_cast(d * sc[0] * ((int8_t)((ql[ 0] & 0xF) | (((qh >> 0) & 3) << 4)) - 32)); + if (y_offset + 32 < ne00) dst_row[y_offset + 32] = ggml_cuda_cast(d * sc[2] * ((int8_t)((ql[32] & 0xF) | (((qh >> 2) & 3) << 4)) - 32)); + if (y_offset + 64 < ne00) dst_row[y_offset + 64] = ggml_cuda_cast(d * sc[4] * ((int8_t)((ql[ 0] >> 4) | (((qh >> 4) & 3) << 4)) - 32)); + if (y_offset + 96 < ne00) dst_row[y_offset + 96] = ggml_cuda_cast(d * sc[6] * ((int8_t)((ql[32] >> 4) | (((qh >> 6) & 3) << 4)) - 32)); + } +} + +template +static void get_rows_cuda_q6_K( + const void * src0_d, const int32_t * src1_d, dst_t * dst_d, + const int64_t ne00, const size_t nb01, const size_t nb02, const size_t nb03, + const int64_t ne10, const int64_t ne11, const int64_t ne12, const size_t nb10, const size_t nb11, const size_t nb12, + const size_t nb1, const size_t nb2, const size_t nb3, + cudaStream_t stream) { + const int64_t nb_blocks = ne00 / QK_K; + const dim3 block_dims(64, 1, 1); + const dim3 block_nums(ne10, MIN(nb_blocks, (int64_t)UINT16_MAX), MIN(ne11*ne12, (int64_t)UINT16_MAX)); + + const size_t s1 = nb1 / sizeof(dst_t); + const size_t s2 = nb2 / sizeof(dst_t); + const size_t s3 = nb3 / sizeof(dst_t); + + const size_t s10 = nb10 / sizeof(int32_t); + const size_t s11 = nb11 / sizeof(int32_t); + const size_t s12 = nb12 / sizeof(int32_t); + + k_get_rows_q6_K<<>>( + src0_d, src1_d, dst_d, + ne00, ne11, ne12, + s1, s2, s3, + nb01, nb02, nb03, + s10, s11, s12); +} + template static void ggml_cuda_get_rows_switch_src0_type( const void * src0_d, const ggml_type src0_type, const int32_t * src1_d, dst_t * dst_d, @@ -199,8 +274,11 @@ static void ggml_cuda_get_rows_switch_src0_type( get_rows_cuda_q(src0_d, src1_d, dst_d, ne00, nb01, nb02, nb03, ne10, ne11, ne12, nb10, nb11, nb12, nb1, nb2, nb3, stream); break; + case GGML_TYPE_Q6_K: + get_rows_cuda_q6_K(src0_d, src1_d, dst_d, + ne00, nb01, nb02, nb03, ne10, ne11, ne12, nb10, nb11, nb12, nb1, nb2, nb3, stream); + break; default: - // TODO: k-quants GGML_ABORT("%s: unsupported src0 type: %s\n", __func__, ggml_type_name(src0_type)); break; } diff --git a/ml/backend/ggml/ggml/src/ggml-cuda/ggml-cuda.cu b/ml/backend/ggml/ggml/src/ggml-cuda/ggml-cuda.cu index 5c9dfd032..b8ed3709b 100644 --- a/ml/backend/ggml/ggml/src/ggml-cuda/ggml-cuda.cu +++ b/ml/backend/ggml/ggml/src/ggml-cuda/ggml-cuda.cu @@ -4693,6 +4693,7 @@ static bool ggml_backend_cuda_device_supports_op(ggml_backend_dev_t dev, const g case GGML_TYPE_Q5_0: case GGML_TYPE_Q5_1: case GGML_TYPE_Q8_0: + case GGML_TYPE_Q6_K: return true; default: return false; diff --git a/model/model.go b/model/model.go index 42fe7f25c..db398bf1b 100644 --- a/model/model.go +++ b/model/model.go @@ -47,6 +47,12 @@ type Validator interface { Validate() error } +// PostLoader is an optional interface that models can implement to run +// initialization steps after backend weights have been loaded. +type PostLoader interface { + PostLoad() error +} + // MultimodalProcessor must be implemented by multimodal models. type MultimodalProcessor interface { // EncodeMultimodal processes a single input (such as an image) and diff --git a/model/model_test.go b/model/model_test.go index 03b9460d0..7d3e5f07e 100644 --- a/model/model_test.go +++ b/model/model_test.go @@ -68,6 +68,8 @@ func (f *fakeTensor) Fill(ctx ml.Context, _ float32) ml.Tensor func (f *fakeTensor) Repeat4D(ctx ml.Context, _, _, _, _ int) ml.Tensor { return f } func (f *fakeTensor) SolveTri(ctx ml.Context, _ ml.Tensor, _, _, _ bool) ml.Tensor { return f } func (f *fakeTensor) SSMScan(ctx ml.Context, _, _, _, _, _, _ ml.Tensor) ml.Tensor { return f } +func (f *fakeTensor) Conv1DDW(ctx ml.Context, _ ml.Tensor, _, _, _ int) ml.Tensor { return f } +func (f *fakeTensor) PadExt(ctx ml.Context, _, _, _, _, _, _, _, _ int) ml.Tensor { return f } func (m *fakeBackend) Get(name string) ml.Tensor { if slices.Contains(m.names, name) { diff --git a/model/models/gemma4/model.go b/model/models/gemma4/model.go new file mode 100644 index 000000000..45d986fc7 --- /dev/null +++ b/model/models/gemma4/model.go @@ -0,0 +1,265 @@ +package gemma4 + +import ( + "bytes" + "fmt" + "image" + "log/slog" + "slices" + "time" + + "github.com/ollama/ollama/fs" + "github.com/ollama/ollama/kvcache" + "github.com/ollama/ollama/ml" + "github.com/ollama/ollama/ml/nn" + "github.com/ollama/ollama/ml/nn/rope" + "github.com/ollama/ollama/model" + "github.com/ollama/ollama/model/input" + "github.com/ollama/ollama/tokenizer" +) + +type Model struct { + model.Base + tokenizer.Tokenizer + + *VisionModel `gguf:"v"` + *TextModel + *AudioModel `gguf:"a"` + + *MultiModalProjector `gguf:"mm"` + *AudioMultimodalProjector `gguf:"mm.a"` + + ImageProcessor + + imageTokenID int32 + imageEndTokenID int32 + audioTokenID int32 + audioEndTokenID int32 + + audioOpts *AudioModelOptions +} + +var _ model.MultimodalProcessor = (*Model)(nil) + +type MultiModalProjector struct { + Projection *ClippableLinear `gguf:"input_projection"` +} + +func (p *MultiModalProjector) Forward(ctx ml.Context, visionOutputs ml.Tensor, eps float32) ml.Tensor { + visionOutputs = p.Projection.Forward(ctx, visionOutputs) + // Post-projection RMSNorm without learned weight + visionOutputs = visionOutputs.RMSNorm(ctx, nil, eps) + return visionOutputs +} + +func New(c fs.Config) (model.Model, error) { + vocabulary := tokenizer.Vocabulary{ + Values: c.Strings("tokenizer.ggml.tokens"), + Scores: c.Floats("tokenizer.ggml.scores"), + Types: c.Ints("tokenizer.ggml.token_type"), + Merges: c.Strings("tokenizer.ggml.merges"), + AddBOS: c.Bool("tokenizer.ggml.add_bos_token", false), + BOS: []int32{int32(c.Uint("tokenizer.ggml.bos_token_id"))}, + AddEOS: c.Bool("tokenizer.ggml.add_eos_token", false), + EOS: append( + []int32{ + int32(c.Uint("tokenizer.ggml.eos_token_id")), + }, + c.Ints("tokenizer.ggml.eos_token_ids")..., + ), + } + + vocabulary.EOS = append(vocabulary.EOS, int32(c.Uint("tokenizer.ggml.eot_token_id", 106))) + + // Gemma 4 uses BPE with SentencePiece-style ▁ space markers (not GPT-2 byte-level encoding). + // The tokenizer.json has merges and a Replace normalizer (space → ▁), with no pre-tokenizer. + t := tokenizer.NewBytePairEncodingWithOptions(&vocabulary, []string{}, + tokenizer.WithSentencePieceNormalizer()) + + // Look up special token IDs for vision and audio + imageTokenID := int32(-1) + imageEndTokenID := int32(-1) + audioTokenID := int32(-1) + audioEndTokenID := int32(-1) + for i, tok := range vocabulary.Values { + switch tok { + case "<|image>": + imageTokenID = int32(i) + case "": + imageEndTokenID = int32(i) + case "<|audio>": + audioTokenID = int32(i) + case "": + audioEndTokenID = int32(i) + } + } + + slog.Info("gemma4: token IDs", "image", imageTokenID, "image_end", imageEndTokenID, "audio", audioTokenID, "audio_end", audioEndTokenID) + + m := Model{ + Tokenizer: t, + TextModel: newTextModel(c), + VisionModel: newVisionModel(c), + AudioModel: newAudioModel(c), + MultiModalProjector: &MultiModalProjector{}, + AudioMultimodalProjector: &AudioMultimodalProjector{}, + ImageProcessor: newImageProcessor(c), + imageTokenID: imageTokenID, + imageEndTokenID: imageEndTokenID, + audioTokenID: audioTokenID, + audioEndTokenID: audioEndTokenID, + audioOpts: newAudioModelOptions(c), + } + + slidingWindowLen := int32(c.Uint("attention.sliding_window")) + m.Cache = kvcache.NewWrapperCache( + kvcache.NewSWAMemCache(slidingWindowLen, 4096, m.Shift), + kvcache.NewCausalCache(m.Shift), + ) + + return &m, nil +} + +func (m *Model) EncodeMultimodal(ctx ml.Context, multimodalData []byte) ([]input.Multimodal, error) { + // Audio input: detect WAV format and route to audio encoder. + if isAudioData(multimodalData) { + return m.encodeAudioMultimodal(ctx, multimodalData) + } + + if len(m.VisionModel.Layers) == 0 { + return nil, model.ErrNoVisionModel + } + + t0 := time.Now() + img, _, err := image.Decode(bytes.NewReader(multimodalData)) + if err != nil { + return nil, err + } + slog.Info("vision: decode", "elapsed", time.Since(t0), "bounds", img.Bounds()) + + t1 := time.Now() + f32s, imgW, imgH, err := m.ImageProcessor.ProcessImage(img) + if err != nil { + return nil, err + } + slog.Info("vision: preprocess", "elapsed", time.Since(t1), "size", [2]int{imgW, imgH}) + + pixelValues := ctx.Input().FromFloats(f32s, imgW, imgH, m.ImageProcessor.numChannels) + slog.Info("vision: pixelValues", "shape", pixelValues.Shape(), "dim0", pixelValues.Dim(0), "dim1", pixelValues.Dim(1), "dim2", pixelValues.Dim(2)) + + numPatchesX := imgW / m.ImageProcessor.patchSize + numPatchesY := imgH / m.ImageProcessor.patchSize + slog.Info("vision: patches", "patchesX", numPatchesX, "patchesY", numPatchesY, "total", numPatchesX*numPatchesY, "patchSize", m.ImageProcessor.patchSize) + + visionOutputs := m.VisionModel.Forward(ctx, pixelValues, numPatchesX, numPatchesY) + visionOutputs = visionPoolAndProject(ctx, visionOutputs, numPatchesX, numPatchesY, m.VisionModel.VisionModelOptions, m.MultiModalProjector, m.VisionModel.StdBias, m.VisionModel.StdScale) + slog.Info("vision: encoded", "elapsed", time.Since(t0), "shape", visionOutputs.Shape()) + + return []input.Multimodal{{Tensor: visionOutputs}}, nil +} + +func (m *Model) PostLoad() error { + m.VisionModel.InitClamp(m.MultiModalProjector) + return nil +} + +func (m *Model) encodeAudioMultimodal(ctx ml.Context, data []byte) ([]input.Multimodal, error) { + if m.AudioModel == nil || m.audioOpts == nil { + return nil, model.ErrNoVisionModel + } + + t0 := time.Now() + samples, err := decodeWAV(data) + if err != nil { + return nil, err + } + slog.Info("audio: decode", "elapsed", time.Since(t0), "samples", len(samples), "duration_s", float64(len(samples))/audioSampleRate) + + // Pad waveform to next multiple of 128. + if rem := len(samples) % 128; rem != 0 { + samples = append(samples, make([]float32, 128-rem)...) + } + + // Compute mel spectrogram. + melData, numFrames := computeMelSpectrogram(samples) + if numFrames == 0 { + return nil, fmt.Errorf("audio too short to encode") + } + slog.Info("audio: mel", "frames", numFrames, "elapsed", time.Since(t0)) + + // Create input tensor [melBins, numFrames] (GGML ne order). FromFloats creates F32. + melTensor := ctx.Input().FromFloats(melData, melBins, numFrames) + + // Run audio encoder. + audioOutputs := m.AudioModel.ForwardAudio(ctx, melTensor, m.AudioMultimodalProjector, m.audioOpts) + slog.Info("audio: encoded", "elapsed", time.Since(t0), "shape", audioOutputs.Shape()) + + return []input.Multimodal{{Tensor: audioOutputs, Data: audioTag{}}}, nil +} + +// audioTag marks multimodal data as audio (vs vision) for PostTokenize. +type audioTag struct{} + +func (m *Model) PostTokenize(inputs []*input.Input) ([]*input.Input, error) { + var result []*input.Input + + for _, inp := range inputs { + if len(inp.Multimodal) == 0 { + result = append(result, inp) + continue + } + + inputMultimodal := inp.Multimodal[0].Tensor + numTokens := inputMultimodal.Dim(1) + + // Determine if this is audio or vision based on the tag. + _, isAudio := inp.Multimodal[0].Data.(audioTag) + + var beginToken, endToken int32 + if isAudio { + beginToken = m.audioTokenID + endToken = m.audioEndTokenID + } else { + beginToken = m.imageTokenID + endToken = m.imageEndTokenID + } + + if beginToken >= 0 { + result = append(result, &input.Input{Token: beginToken, SameBatch: numTokens + 2}) + } + + result = append(result, + &input.Input{Multimodal: []input.Multimodal{{Tensor: inputMultimodal}}, MultimodalHash: inp.MultimodalHash}, + ) + result = append(result, slices.Repeat([]*input.Input{{Token: 0}}, numTokens-1)...) + + if endToken >= 0 { + result = append(result, &input.Input{Token: endToken}) + } + } + + return result, nil +} + +func (m *Model) Forward(ctx ml.Context, batch input.Batch) (ml.Tensor, error) { + hiddenState := m.TextModel.Forward(ctx, batch, m.Cache) + + hiddenState = m.TextModel.Output.Forward(ctx, hiddenState) + + if m.TextModel.TextOptions.finalLogitSoftcap > 0.0 { + hiddenState = hiddenState.Scale(ctx, 1.0/float64(m.TextModel.TextOptions.finalLogitSoftcap)) + hiddenState = hiddenState.Tanh(ctx) + hiddenState = hiddenState.Scale(ctx, float64(m.TextModel.TextOptions.finalLogitSoftcap)) + } + + return hiddenState, nil +} + +func (m *Model) Shift(ctx ml.Context, layer int, key, shift ml.Tensor) (ml.Tensor, error) { + ropeBase, ropeDims := m.TextModel.ropeForLayer(layer) + return nn.RoPE(ctx, key, shift, ropeDims, ropeBase, 1.0, rope.WithTypeNeoX()), nil +} + +func init() { + model.Register("gemma4", New) +} diff --git a/model/models/gemma4/model_audio.go b/model/models/gemma4/model_audio.go new file mode 100644 index 000000000..2bb53fad7 --- /dev/null +++ b/model/models/gemma4/model_audio.go @@ -0,0 +1,611 @@ +package gemma4 + +import ( + "math" + + "github.com/ollama/ollama/fs" + "github.com/ollama/ollama/ml" + "github.com/ollama/ollama/ml/nn" +) + +// AudioModel holds the audio encoder and configuration. +type AudioModel struct { + // SSCP: Sub-Sample Convolution Projection. + SSCPConv0 *AudioConvBlock `gguf:"conv1d.0"` + SSCPConv1 *AudioConvBlock `gguf:"conv1d.1"` + + // SSCP output projection (linear). + SSCPInputProj *nn.Linear `gguf:"pre_encode.out"` + + // Conformer blocks. + Layers []AudioConformerBlock `gguf:"blk"` + + // Output projection to embedder dimension. + OutputProj *AudioOutputProj `gguf:"output_proj"` + + AudioModelOptions +} + +type AudioOutputProj struct { + Weight ml.Tensor `gguf:"weight"` + Bias ml.Tensor `gguf:"bias"` +} + +// AudioModelOptions holds audio model hyperparameters. +type AudioModelOptions struct { + hiddenSize int + numHeads int + headDim int + ffnSize int + numLayers int + melBins int + chunkSize int + maxPast int + maxFuture int + contextSize int + logitCap float32 + residualWeight float32 + gradClip float32 + convKernelSize int + eps float32 +} + +// AudioConvBlock is a single 2D convolution block for the SSCP. +type AudioConvBlock struct { + Weight ml.Tensor `gguf:"weight"` + Norm *nn.LayerNorm `gguf:"norm"` +} + +// AudioConformerBlock is a single conformer layer. +// All tensors are flat at the block level (a.blk.N.) using underscore naming. +type AudioConformerBlock struct { + // Block-level norm + Norm *nn.RMSNorm `gguf:"layer_pre_norm"` + + // FFW start + FFWNorm *nn.RMSNorm `gguf:"ffn_norm"` + FFWUp *AudioClippableLinear `gguf:"ffn_up"` + FFWDown *AudioClippableLinear `gguf:"ffn_down"` + FFWPostNorm *nn.RMSNorm `gguf:"ffn_post_norm"` + + // FFW end + FFWNorm1 *nn.RMSNorm `gguf:"ffn_norm_1"` + FFWUp1 *AudioClippableLinear `gguf:"ffn_up_1"` + FFWDown1 *AudioClippableLinear `gguf:"ffn_down_1"` + FFWPostNorm1 *nn.RMSNorm `gguf:"ffn_post_norm_1"` + + // Attention + AttnQ *AudioClippableLinear `gguf:"attn_q"` + AttnK *AudioClippableLinear `gguf:"attn_k"` + AttnV *AudioClippableLinear `gguf:"attn_v"` + AttnOut *AudioClippableLinear `gguf:"attn_out"` + AttnPreNorm *nn.RMSNorm `gguf:"ln1"` + AttnPostNorm *nn.RMSNorm `gguf:"ln2"` + LinearPos ml.Tensor `gguf:"linear_pos.weight"` + PerDimScale ml.Tensor `gguf:"per_dim_scale.weight"` + + // LightConv1d + ConvPW1 *AudioClippableLinear `gguf:"conv_pw1"` + ConvPW2 *AudioClippableLinear `gguf:"conv_pw2"` + ConvDW ml.Tensor `gguf:"conv_dw.weight"` + ConvNorm *nn.RMSNorm `gguf:"conv_norm"` + NormConv *nn.RMSNorm `gguf:"norm_conv"` +} + +// AudioClippableLinear is a linear layer with optional input/output clamping. +type AudioClippableLinear struct { + Weight ml.Tensor `gguf:"weight"` + Bias ml.Tensor `gguf:"bias"` + InputMin ml.Tensor `gguf:"input_min"` + InputMax ml.Tensor `gguf:"input_max"` + OutputMin ml.Tensor `gguf:"output_min"` + OutputMax ml.Tensor `gguf:"output_max"` + + // Cached scalar clamp values (populated on first forward). + inMin, inMax, outMin, outMax float32 + clampsLoaded bool +} + +func (l *AudioClippableLinear) loadClamps() { + if l.clampsLoaded { + return + } + l.clampsLoaded = true + if l.InputMin != nil { + vals := l.InputMin.BackendGet() + if len(vals) > 0 { + l.inMin = vals[0] + } + } + if l.InputMax != nil { + vals := l.InputMax.BackendGet() + if len(vals) > 0 { + l.inMax = vals[0] + } + } + if l.OutputMin != nil { + vals := l.OutputMin.BackendGet() + if len(vals) > 0 { + l.outMin = vals[0] + } + } + if l.OutputMax != nil { + vals := l.OutputMax.BackendGet() + if len(vals) > 0 { + l.outMax = vals[0] + } + } +} + +func (l *AudioClippableLinear) Forward(ctx ml.Context, x ml.Tensor) ml.Tensor { + l.loadClamps() + if l.inMax != 0 { + x = x.Clamp(ctx, l.inMin, l.inMax) + } + out := l.Weight.Mulmat(ctx, x) + if l.Bias != nil { + out = out.Add(ctx, l.Bias) + } + if l.outMax != 0 { + out = out.Clamp(ctx, l.outMin, l.outMax) + } + return out +} + +// AudioMultimodalProjector is the audio-to-text embedding projector. +type AudioMultimodalProjector struct { + Projection *AudioClippableLinear `gguf:"input_projection"` + FC *AudioFC `gguf:"fc"` +} + +type AudioFC struct { + Weight ml.Tensor `gguf:"weight"` + Bias ml.Tensor `gguf:"bias"` +} + +func (p *AudioMultimodalProjector) Forward(ctx ml.Context, x ml.Tensor, eps float32) ml.Tensor { + // FC: output projection from conformer to embedder dimension. + x = p.FC.Weight.Mulmat(ctx, x) + if p.FC.Bias != nil { + x = x.Add(ctx, p.FC.Bias) + } + // Pre-projection RMSNorm (without learned weight) — matches Python's embedding_pre_projection_norm. + x = x.RMSNorm(ctx, nil, eps) + // Embedding projection to text hidden size. + x = p.Projection.Forward(ctx, x) + return x +} + +// ForwardAudio encodes mel spectrogram features into soft tokens. +// melFeatures: float32 tensor with ne[0]=melBins, ne[1]=numFrames. +// Returns: [hiddenSize, numTokens] tensor. +func (m *AudioModel) ForwardAudio(ctx ml.Context, melFeatures ml.Tensor, proj *AudioMultimodalProjector, opts *AudioModelOptions) ml.Tensor { + // SSCP Conv2D input: ne[0]=F (freq/width), ne[1]=T (time/height), ne[2]=C_in, ne[3]=B + // melFeatures is [melBins, numFrames], add channel and batch dims. + x := melFeatures.Reshape(ctx, melFeatures.Dim(0), melFeatures.Dim(1), 1, 1) + + // SSCP Conv block 0: [F, T, 1, 1] → [F', T', C0, 1] + x = forwardConvBlock(ctx, m.SSCPConv0, x, opts) + + // SSCP Conv block 1: [F', T', C0, 1] → [F'', T'', C1, 1] + x = forwardConvBlock(ctx, m.SSCPConv1, x, opts) + + // After conv blocks, layout is [F'', T'', C_out, B]. + // Permute to [C_out*F'', T'', B] for linear projection (channels+freq in ne[0]). + fOut := x.Dim(0) + tOut := x.Dim(1) + cOut := x.Dim(2) + // Permute [F'', T'', C, B] → [C, F'', T'', B] + // (1,2,0,3): old[0]→pos1, old[1]→pos2, old[2]→pos0 + x = x.Permute(ctx, 1, 2, 0, 3).Contiguous(ctx) + x = x.Reshape(ctx, cOut*fOut, tOut) + + // Linear projection to hidden size. + x = m.SSCPInputProj.Forward(ctx, x) + + // Build causal-valid mask for conformer attention. + causalMask := buildCausalValidMaskF32(opts.chunkSize, opts.maxPast, opts.maxFuture) + + // Run conformer blocks. + for i := range m.Layers { + x = m.Layers[i].Forward(ctx, x, causalMask, opts, i) + } + + // Output projection. + if m.OutputProj != nil { + x = m.OutputProj.Weight.Mulmat(ctx, x) + if m.OutputProj.Bias != nil { + x = x.Add(ctx, m.OutputProj.Bias) + } + } + + // Audio embedder: project to text embedding space. + if proj != nil { + x = proj.Forward(ctx, x, opts.eps) + } + + return x +} + +// forwardConvBlock runs a single SSCP Conv2D block. +// Conv2D receiver is the kernel, argument is the input data. +// Input: [F, T, C_in, B]. Output: [F', T', C_out, B]. +func forwardConvBlock(ctx ml.Context, block *AudioConvBlock, x ml.Tensor, opts *AudioModelOptions) ml.Tensor { + // Conv2D: kernel.Conv2D(ctx, input, s0, s1, p0, p1, d0, d1) + // Kernel is 3x3, stride 2x2, padding 1x1 (matching SSCP config). + // Output layout: [F', T', C_out, B] + // Make weight contiguous — the shape reversal in the converter creates + // a tensor where the physical data order doesn't match ne[]/stride[]. + weight := block.Weight.Contiguous(ctx) + x = weight.Conv2D(ctx, x, 2, 2, 1, 1, 1, 1) + + // LayerNorm needs channels in ne[0]. Permute [F', T', C_out, B] → [C_out, F', T', B], + // norm, then permute back. + // GGML permute: axis i says where old axis i goes. + // (1,2,0,3): old[0]→pos1, old[1]→pos2, old[2]→pos0 → [C_out, F', T', B] + x = x.Permute(ctx, 1, 2, 0, 3).Contiguous(ctx) + x = block.Norm.Forward(ctx, x, opts.eps) + // (2,0,1,3): old[0]→pos2, old[1]→pos0, old[2]→pos1 → [F', T', C_out, B] + x = x.Permute(ctx, 2, 0, 1, 3).Contiguous(ctx) + + x = x.RELU(ctx) + return x +} + +// Forward runs a single conformer block. +func (cb *AudioConformerBlock) Forward(ctx ml.Context, x ml.Tensor, causalMask []float32, opts *AudioModelOptions, blockIdx int) ml.Tensor { + // FFW start (half-residual). + x = cb.forwardFFW(ctx, cb.FFWNorm, cb.FFWUp, cb.FFWDown, cb.FFWPostNorm, x, opts) + + // Self-attention. + x = cb.forwardAttention(ctx, x, causalMask, opts, blockIdx) + + // Lightweight Conv1d. + x = cb.forwardLightConv(ctx, x, opts, blockIdx) + + // FFW end (half-residual). + x = cb.forwardFFW(ctx, cb.FFWNorm1, cb.FFWUp1, cb.FFWDown1, cb.FFWPostNorm1, x, opts) + + // Gradient clipping + final norm. + x = x.Clamp(ctx, -opts.gradClip, opts.gradClip) + x = cb.Norm.Forward(ctx, x, opts.eps) + + return x +} + +// forwardFFW runs a feedforward module with half-residual connection. +func (cb *AudioConformerBlock) forwardFFW(ctx ml.Context, preNorm *nn.RMSNorm, up, down *AudioClippableLinear, postNorm *nn.RMSNorm, x ml.Tensor, opts *AudioModelOptions) ml.Tensor { + residual := x + x = x.Clamp(ctx, -opts.gradClip, opts.gradClip) + x = preNorm.Forward(ctx, x, opts.eps) + x = up.Forward(ctx, x) + x = x.SILU(ctx) + x = down.Forward(ctx, x) + x = x.Clamp(ctx, -opts.gradClip, opts.gradClip) + x = postNorm.Forward(ctx, x, opts.eps) + x = x.Scale(ctx, float64(opts.residualWeight)) + return residual.Add(ctx, x) +} + +// forwardAttention runs the conformer block-local attention with relative position embeddings. +func (cb *AudioConformerBlock) forwardAttention(ctx ml.Context, x ml.Tensor, causalMask []float32, opts *AudioModelOptions, blockIdx int) ml.Tensor { + residual := x + x = x.Clamp(ctx, -opts.gradClip, opts.gradClip) + x = cb.AttnPreNorm.Forward(ctx, x, opts.eps) + + hiddenSize := x.Dim(0) + seqLen := x.Dim(1) + + // QKV projections: [hiddenSize, seqLen] → [headDim, numHeads, seqLen] + q := cb.AttnQ.Forward(ctx, x).Reshape(ctx, opts.headDim, opts.numHeads, seqLen) + k := cb.AttnK.Forward(ctx, x).Reshape(ctx, opts.headDim, opts.numHeads, seqLen) + v := cb.AttnV.Forward(ctx, x).Reshape(ctx, opts.headDim, opts.numHeads, seqLen) + + // Per-dim scaling for queries: (headDim^-0.5 / log(2)) * softplus(per_dim_scale) + // per_dim_scale is already softplus'd from the converter. + qScale := float64(math.Pow(float64(opts.headDim), -0.5)) / math.Log(2) + q = q.Scale(ctx, qScale) + if cb.PerDimScale != nil { + q = q.Mul(ctx, cb.PerDimScale) + } + + // Key scaling: softplus(1) / log(2) — matches the query base scaling convention. + kScale := math.Log(1+math.E) / math.Log(2) + k = k.Scale(ctx, kScale) + + // Build sinusoidal position embeddings for the block-local context. + maxSpan := opts.maxPast + opts.maxFuture + 1 // 13 unique relative positions + posEmb := cb.buildPositionEmbeddings(ctx, maxSpan, opts) + // posEmb: [headDim, numHeads, maxSpan] + + // Block-local attention: process chunks of size chunkSize. + chunkSize := opts.chunkSize + numChunks := (seqLen + chunkSize - 1) / chunkSize + contextSize := opts.contextSize + + // Pad q/k/v to multiple of chunkSize on the time dimension (dim 2). + padT := numChunks*chunkSize - seqLen + if padT > 0 { + q = q.Pad(ctx, 0, 0, padT, 0) + k = k.Pad(ctx, 0, 0, padT, 0) + v = v.Pad(ctx, 0, 0, padT, 0) + } + paddedLen := numChunks * chunkSize + + // Pad k/v for context extraction: add maxPast on left, (maxFuture+chunkSize-1) on right. + // Use Pad (right) + PadExt (left) workaround since PadExt+Slice has issues. + // Actually use Concat with zero tensors for reliable left-padding. + padLeft := opts.maxPast + padRight := opts.maxFuture + chunkSize - 1 + zeroLeft := ctx.Input().FromFloats(make([]float32, opts.headDim*opts.numHeads*padLeft), opts.headDim, opts.numHeads, padLeft) + zeroRight := ctx.Input().FromFloats(make([]float32, opts.headDim*opts.numHeads*padRight), opts.headDim, opts.numHeads, padRight) + kPadded := zeroLeft.Concat(ctx, k, 2).Concat(ctx, zeroRight, 2) + vPadded := zeroLeft.Concat(ctx, v, 2).Concat(ctx, zeroRight, 2) + + // Reshape q into chunks: [headDim, numHeads, numChunks, chunkSize] + qChunked := q.Reshape(ctx, opts.headDim, opts.numHeads, numChunks, chunkSize) + + // Process each chunk and collect results. + chunkOutputs := make([]ml.Tensor, numChunks) + for u := range numChunks { + // Extract query block: [headDim, numHeads, 1, chunkSize] → [headDim, numHeads, chunkSize] + qBlock := qChunked.Slice(ctx, 2, u, u+1, 1).Reshape(ctx, opts.headDim, opts.numHeads, chunkSize) + + // Extract key/value context: [headDim, numHeads, contextSize] + cStart := u * chunkSize // offset in kPadded (padLeft already accounts for left context) + kCtx := kPadded.Slice(ctx, 2, cStart, cStart+contextSize, 1).Contiguous(ctx) + vCtx := vPadded.Slice(ctx, 2, cStart, cStart+contextSize, 1).Contiguous(ctx) + + // Content-content logits: qBlock^T @ kCtx → [chunkSize, contextSize] per head. + // Mulmat(a, b) = a^T @ b. We want Q^T K, so: kCtx.Mulmat(qBlock) but that gives + // [numHeads, chunkSize, contextSize] with wrong batching. + // Instead: permute to [headDim, chunkSize, numHeads] and [headDim, contextSize, numHeads] + // then Mulmat batches over numHeads. + // GGML permute(0,2,1,3): old[0]→0, old[1]→2, old[2]→1 + qP := qBlock.Permute(ctx, 0, 2, 1, 3) // [headDim, chunkSize, numHeads] + kP := kCtx.Permute(ctx, 0, 2, 1, 3) // [headDim, contextSize, numHeads] + + termAC := kP.MulmatFullPrec(ctx, qP) // [contextSize, chunkSize, numHeads] + + // Content-position logits: qBlock^T @ posEmb → [chunkSize, maxSpan] per head. + pP := posEmb.Permute(ctx, 0, 2, 1, 3) // [headDim, maxSpan, numHeads] + termBDRaw := pP.MulmatFullPrec(ctx, qP) // [maxSpan, chunkSize, numHeads] + + // Relative shift: [maxSpan, chunkSize, numHeads] → [contextSize, chunkSize, numHeads] + termBD := cb.relativeShiftGGML(ctx, termBDRaw, maxSpan, chunkSize, contextSize, opts.numHeads) + + // Combined logits. + logits := termAC.Add(ctx, termBD) + + // Logit softcap: tanh(logits / cap) * cap + logits = logits.Scale(ctx, 1.0/float64(opts.logitCap)) + logits = logits.Tanh(ctx) + logits = logits.Scale(ctx, float64(opts.logitCap)) + + // Apply combined causal + validity mask. + // causalMask [chunkSize * contextSize]: 1=causal-allowed, 0=masked. + // Validity: context positions before the actual sequence start are invalid. + // For chunk u, context position c corresponds to actual time: u*chunkSize + c - padLeft. + // Valid if 0 <= actual_time < seqLen. + // Mask tensor layout: [contextSize, chunkSize, 1] with ne[0]=contextSize contiguous. + // Element at (context=j, chunk=i) is at flat index: i*contextSize + j. + maskData := make([]float32, contextSize*chunkSize) + for i := range chunkSize { + for j := range contextSize { + actualTime := u*chunkSize + j - padLeft + causalOK := causalMask[i*contextSize+j] > 0 + validOK := actualTime >= 0 && actualTime < seqLen + if causalOK && validOK { + maskData[i*contextSize+j] = 0 + } else { + maskData[i*contextSize+j] = -1e9 + } + } + } + mask := ctx.Input().FromFloats(maskData, contextSize, chunkSize, 1) // 3D for broadcasting over numHeads + logits = logits.Add(ctx, mask) + + // Softmax over context dimension (dim 0 = contextSize). + logits = logits.Softmax(ctx) // softmax over ne[0]=contextSize + + // Weighted sum: logits^T @ vCtx. + // logits: [contextSize, chunkSize, numHeads], vCtx: [headDim, numHeads, contextSize] + // vCtx permuted: [headDim, contextSize, numHeads] + vP := vCtx.Permute(ctx, 0, 2, 1, 3) // [headDim, contextSize, numHeads] + // Weighted sum: for each head, value[headDim, contextSize] @ weights[contextSize, chunkSize] + // = [headDim, chunkSize]. + // Mulmat(a, b) = a^T @ b. Need a=[contextSize, headDim, numHeads], b=[contextSize, chunkSize, numHeads]. + vPT := vP.Permute(ctx, 1, 0, 2, 3).Contiguous(ctx) // [contextSize, headDim, numHeads] + chunkOut := vPT.Mulmat(ctx, logits) // [headDim, chunkSize, numHeads] + + // Permute back to [headDim, numHeads, chunkSize] + chunkOut = chunkOut.Permute(ctx, 0, 2, 1, 3).Contiguous(ctx) + chunkOutputs[u] = chunkOut + } + + // Concatenate chunk outputs along time dimension. + var attnOut ml.Tensor + if numChunks == 1 { + attnOut = chunkOutputs[0] + } else { + attnOut = chunkOutputs[0] + for _, co := range chunkOutputs[1:] { + attnOut = attnOut.Concat(ctx, co, 2) + } + } + + // Trim to original sequence length if we padded. + if paddedLen > seqLen { + attnOut = attnOut.Slice(ctx, 2, 0, seqLen, 1).Contiguous(ctx) + } + + // Reshape to [hiddenSize, seqLen] and project. + attnOut = attnOut.Reshape(ctx, hiddenSize, seqLen) + x = cb.AttnOut.Forward(ctx, attnOut) + x = x.Clamp(ctx, -opts.gradClip, opts.gradClip) + x = cb.AttnPostNorm.Forward(ctx, x, opts.eps) + + return residual.Add(ctx, x) +} + +// buildPositionEmbeddings builds sinusoidal position embeddings and projects through linear_pos. +// Returns [headDim, numHeads, maxSpan] tensor. +func (cb *AudioConformerBlock) buildPositionEmbeddings(ctx ml.Context, maxSpan int, opts *AudioModelOptions) ml.Tensor { + halfDim := opts.hiddenSize / 2 + hiddenSize := opts.hiddenSize + + // inv_timescales: exp(-i * log(10000) / max(D/2-1, 1)) + logInc := math.Log(10000.0) / math.Max(float64(halfDim-1), 1) + + // Sinusoidal embeddings for relative positions [maxPast, maxPast-1, ..., -maxFuture]. + posData := make([]float32, hiddenSize*maxSpan) + for p := range maxSpan { + relPos := float64(opts.maxPast - p) + for d := range halfDim { + angle := relPos * math.Exp(float64(-d)*logInc) + posData[p*hiddenSize+d] = float32(math.Sin(angle)) + posData[p*hiddenSize+halfDim+d] = float32(math.Cos(angle)) + } + } + + // Create [hiddenSize, maxSpan] input tensor. + posEmb := ctx.Input().FromFloats(posData, hiddenSize, maxSpan) + + // Project through linear_pos: [hiddenSize, maxSpan] → Mulmat → [numHeads*headDim, maxSpan] + projPos := cb.LinearPos.Mulmat(ctx, posEmb) + + // Reshape to [headDim, numHeads, maxSpan]. + return projPos.Reshape(ctx, opts.headDim, opts.numHeads, maxSpan) +} + +// relativeShiftGGML performs the relative shift to extract correct position logits. +// Input: [maxSpan, chunkSize, numHeads]. Output: [contextSize, chunkSize, numHeads]. +func (cb *AudioConformerBlock) relativeShiftGGML(ctx ml.Context, x ml.Tensor, maxSpan, chunkSize, contextSize, numHeads int) ml.Tensor { + // The shift trick: pad ne[0] to contextSize+1, reshape to flatten first two dims, + // skip first (contextSize+1-maxSpan) elements, take contextSize*chunkSize elements, reshape back. + padAmt := contextSize + 1 - maxSpan + if padAmt > 0 { + x = x.Pad(ctx, padAmt, 0, 0, 0) // [maxSpan+padAmt, chunkSize, numHeads] = [contextSize+1, chunkSize, numHeads] + } + // Reshape to [(contextSize+1)*chunkSize, numHeads] + x = x.Reshape(ctx, (contextSize+1)*chunkSize, numHeads) + // Take the first contextSize*chunkSize elements (the standard relative shift trick). + x = x.Slice(ctx, 0, 0, contextSize*chunkSize, 1).Contiguous(ctx) + // Reshape to [contextSize, chunkSize, numHeads] + return x.Reshape(ctx, contextSize, chunkSize, numHeads) +} + +// forwardLightConv runs the lightweight depthwise convolution module. +func (cb *AudioConformerBlock) forwardLightConv(ctx ml.Context, x ml.Tensor, opts *AudioModelOptions, blockIdx int) ml.Tensor { + residual := x + + x = cb.ConvNorm.Forward(ctx, x, opts.eps) + x = cb.ConvPW1.Forward(ctx, x) // [2*D, T, B] + + // GLU: split in half along dim 0, sigmoid gate, multiply. + d := x.Dim(0) / 2 + data := x.Slice(ctx, 0, 0, d, 1).Contiguous(ctx) + gate := x.Slice(ctx, 0, d, d*2, 1).Contiguous(ctx).Sigmoid(ctx) + x = data.Mul(ctx, gate) // [D, T, B] + + // Depthwise Conv1d: manual implementation using model weight tensor slices. + // Kernel cb.ConvDW shape: [K=5, D=1024] (ne[0]=K, ne[1]=D) after shape reversal. + // Actually in GGML, ne[0]=K=5 contiguous, ne[1]=D=1024. + // We need per-tap weights [D] and shifted input copies. + kernelSize := cb.ConvDW.Dim(0) // K=5 + seqLen := x.Dim(1) + + // Transpose kernel to [D, K] for per-tap slicing. + // GGML permute(1,0,2,3): old[0]→pos1, old[1]→pos0 → swap ne[0] and ne[1] + kernelT := cb.ConvDW.Permute(ctx, 1, 0, 2, 3).Contiguous(ctx) // [D, K] + + var convOut ml.Tensor + for k := range kernelSize { + shift := kernelSize - 1 - k + var shifted ml.Tensor + if shift == 0 { + shifted = x + } else { + trimmed := x.Slice(ctx, 1, 0, seqLen-shift, 1).Contiguous(ctx) + shifted = trimmed.PadExt(ctx, 0, 0, shift, 0, 0, 0, 0, 0) + } + + wk := kernelT.Slice(ctx, 1, k, k+1, 1).Contiguous(ctx) // [D, 1] + term := shifted.Mul(ctx, wk) + if convOut == nil { + convOut = term + } else { + convOut = convOut.Add(ctx, term) + } + } + x = convOut + + x = x.Clamp(ctx, -opts.gradClip, opts.gradClip) + x = cb.NormConv.Forward(ctx, x, opts.eps) + x = x.SILU(ctx) + x = cb.ConvPW2.Forward(ctx, x) + + return x.Add(ctx, residual) +} + +func newAudioModel(c fs.Config) *AudioModel { + numLayers := int(c.Uint("audio.block_count", 0)) + if numLayers == 0 { + return nil + } + return &AudioModel{ + Layers: make([]AudioConformerBlock, numLayers), + } +} + +func newAudioModelOptions(c fs.Config) *AudioModelOptions { + hiddenSize := int(c.Uint("audio.embedding_length", 0)) + if hiddenSize == 0 { + return nil + } + numHeads := int(c.Uint("audio.attention.head_count", 8)) + headDim := hiddenSize / numHeads + chunkSize := 12 // default conformer chunk size + maxPast := 12 // conf_attention_context_left - 1 + maxFuture := 0 // conf_attention_context_right + convKernel := int(c.Uint("audio.conv_kernel_size", 5)) + + eps := c.Float("audio.attention.layer_norm_epsilon", 1e-6) + + return &AudioModelOptions{ + hiddenSize: hiddenSize, + numHeads: numHeads, + headDim: headDim, + ffnSize: int(c.Uint("audio.feed_forward_length", uint32(hiddenSize*4))), + numLayers: int(c.Uint("audio.block_count", 12)), + melBins: int(c.Uint("audio.num_mel_bins", 128)), + chunkSize: chunkSize, + maxPast: maxPast, + maxFuture: maxFuture, + contextSize: chunkSize + maxPast + maxFuture, + logitCap: 50.0, + residualWeight: 0.5, + gradClip: 1e10, + convKernelSize: convKernel, + eps: float32(eps), + } +} + +// buildCausalValidMaskF32 creates the causal-valid mask for block-local attention. +// Returns flat [chunkSize * contextSize] float32 data (1.0 = allowed, 0.0 = masked). +func buildCausalValidMaskF32(chunkSize, maxPast, maxFuture int) []float32 { + contextSize := chunkSize + maxPast + maxFuture + upperDiag := maxPast + maxFuture + + result := make([]float32, chunkSize*contextSize) + for r := range chunkSize { + for c := range contextSize { + lower := (r <= c) // tril(contextSize, chunkSize) transposed + upper := (c <= r+upperDiag) // tril(chunkSize, contextSize, diag=upperDiag) + if lower && upper { + result[r*contextSize+c] = 1.0 + } + } + } + return result +} diff --git a/model/models/gemma4/model_text.go b/model/models/gemma4/model_text.go new file mode 100644 index 000000000..86397affd --- /dev/null +++ b/model/models/gemma4/model_text.go @@ -0,0 +1,475 @@ +package gemma4 + +import ( + "math" + + "github.com/ollama/ollama/fs" + "github.com/ollama/ollama/kvcache" + "github.com/ollama/ollama/ml" + "github.com/ollama/ollama/ml/nn" + "github.com/ollama/ollama/ml/nn/rope" + "github.com/ollama/ollama/model/input" +) + +const ( + cacheTypeSWA = iota + cacheTypeCausal +) + +type TextOptions struct { + hiddenSize int + numHeads, numKVHeads int + numGlobalKVHeads int + headDim, globalHeadDim int + hiddenLayers int + hiddenSizePerLayerInput int + + eps float32 + ropeBase float32 + ropeLocalBase float32 + partialRotaryDims int // RoPE dims for full-attention (global) layers + + slidingWindowPattern []bool + // kvDonorMap maps shared layer index -> donor layer index. + // Donor is the last non-shared layer of the same type (sliding/full). + kvDonorMap map[int]int + + finalLogitSoftcap float32 + + numExperts int + numExpertsUsed int +} + +func (o *TextOptions) isLocal(layer int) bool { + if layer < len(o.slidingWindowPattern) { + return o.slidingWindowPattern[layer] + } + return false +} + +func (o *TextOptions) ropeForLayer(layer int) (base float32, dims int) { + if o.isLocal(layer) { + return o.ropeLocalBase, o.headDim + } + return o.ropeBase, o.partialRotaryDims +} + +func (o *TextOptions) kvHeadsForLayer(layer int) int { + if o.isLocal(layer) { + return o.numKVHeads + } + if o.numGlobalKVHeads > 0 { + return o.numGlobalKVHeads + } + return o.numKVHeads +} + +func (o *TextOptions) headDimForLayer(layer int) int { + if o.isLocal(layer) { + return o.headDim + } + return o.globalHeadDim +} + +type TextModel struct { + TokenEmbedding *nn.Embedding `gguf:"token_embd"` + *PerLayerProjector + Layers []TextLayer `gguf:"blk"` + OutputNorm *nn.RMSNorm `gguf:"output_norm"` + Output *nn.Linear `gguf:"output,alt:token_embd"` + TextOptions +} + +func newTextModel(c fs.Config) *TextModel { + numLayers := int(c.Uint("block_count")) + + // Head dimensions: key_length is global head dim, key_length_swa is local (SWA) head dim. + globalHeadDim := int(c.Uint("attention.key_length", 512)) + headDim := int(c.Uint("attention.key_length_swa", 256)) + + // RoPE dimensions for global (full attention) layers with proportional RoPE. + // The freq_factors tensor handles partial rotation (1.0 for rotated pairs, + // 1e30 for non-rotated), so ropeDims equals the full global head dim. + partialRotaryDims := int(c.Uint("rope.dimension_count", 0)) + if partialRotaryDims == 0 { + partialFactor := c.Float("rope.partial_rotary_factor", 1.0) + partialRotaryDims = int(float32(globalHeadDim) * partialFactor) + } + + ropeBase := c.Float("rope.freq_base", 1000000.0) + ropeLocalBase := c.Float("rope.freq_base_swa", 0) + if ropeLocalBase == 0 { + ropeLocalBase = c.Float("rope.local.freq_base", 10000.0) + } + + numGlobalKVHeads := int(c.Uint("attention.global_head_count_kv", 0)) + slidingPattern := c.Bools("attention.sliding_window_pattern") + + // KV heads: try per-layer array first (MoE models), then fall back to scalar + numKVHeads := 0 + kvHeadsArray := c.Ints("attention.head_count_kv") + if len(kvHeadsArray) > 0 { + numKVHeads = int(kvHeadsArray[0]) + if numGlobalKVHeads == 0 && len(slidingPattern) > 0 { + for i, isLocal := range slidingPattern { + if !isLocal && i < len(kvHeadsArray) { + numGlobalKVHeads = int(kvHeadsArray[i]) + break + } + } + } + } + if numKVHeads == 0 { + numKVHeads = int(c.Uint("attention.head_count_kv", 0)) + } + + // Compute KV sharing donor map (same logic as MLX) + sharedLayers := int(c.Uint("attention.shared_kv_layers", 0)) + kvDonorMap := make(map[int]int) + if sharedLayers > 0 && len(slidingPattern) > 0 { + firstShared := numLayers - sharedLayers + for i := firstShared; i < numLayers; i++ { + isLocal := slidingPattern[i] + // Find last non-shared layer of same type + for j := firstShared - 1; j >= 0; j-- { + if slidingPattern[j] == isLocal { + kvDonorMap[i] = j + break + } + } + } + } + + return &TextModel{ + Layers: make([]TextLayer, numLayers), + TextOptions: TextOptions{ + hiddenSize: int(c.Uint("embedding_length")), + numHeads: int(c.Uint("attention.head_count")), + numKVHeads: numKVHeads, + numGlobalKVHeads: numGlobalKVHeads, + headDim: headDim, + globalHeadDim: globalHeadDim, + hiddenLayers: numLayers, + hiddenSizePerLayerInput: int(c.Uint("embedding_length_per_layer_input", 0)), + eps: c.Float("attention.layer_norm_rms_epsilon", 1e-06), + ropeBase: ropeBase, + ropeLocalBase: ropeLocalBase, + partialRotaryDims: partialRotaryDims, + slidingWindowPattern: slidingPattern, + kvDonorMap: kvDonorMap, + finalLogitSoftcap: c.Float("final_logit_softcapping", 0.0), + numExperts: int(c.Uint("expert_count", 0)), + numExpertsUsed: int(c.Uint("expert_used_count", 0)), + }, + } +} + +func (m *TextModel) Forward(ctx ml.Context, batch input.Batch, cache kvcache.Cache) ml.Tensor { + positions := ctx.Input().FromInts(batch.Positions, len(batch.Positions)) + + hiddenState := m.TokenEmbedding.Forward(ctx, batch.Inputs) + hiddenState = hiddenState.Scale(ctx, math.Sqrt(float64(m.hiddenSize))) + + // Inject vision embeddings into the hidden state + var except []int + for _, image := range batch.Multimodal { + visionOutputs := image.Multimodal[0].Tensor + ctx.Forward(visionOutputs.Copy(ctx, hiddenState.View(ctx, image.Index*hiddenState.Stride(1), visionOutputs.Dim(0)*visionOutputs.Dim(1)))) + + for i := range visionOutputs.Dim(1) { + except = append(except, image.Index+i) + } + } + + // PLE + var perLayerInputs ml.Tensor + if m.PerLayerProjector != nil { + perLayerInputs = m.PerLayerProjector.Forward(ctx, batch, hiddenState, &m.TextOptions) + } + + for i := range len(m.Layers) { + layer := m.Layers[i] + if cache != nil { + cache.SetLayer(i) + cacheType := cacheTypeSWA + if !m.isLocal(i) { + cacheType = cacheTypeCausal + } + wc := cache.(*kvcache.WrapperCache) + wc.SetLayerType(cacheType) + + if causal, ok := wc.UnderlyingCache().(*kvcache.Causal); ok { + causal.SetCausal(ctx, kvcache.CausalOptions{Except: except}) + } + } + + var lastLayerOutputs ml.Tensor + if i == len(m.Layers)-1 { + lastLayerOutputs = batch.Outputs + } + + var perLayerInput ml.Tensor + if perLayerInputs != nil { + perLayerInput = perLayerInputs.View(ctx, i*perLayerInputs.Stride(1), perLayerInputs.Dim(0), perLayerInputs.Stride(2), perLayerInputs.Dim(2)) + } + + // KV sharing: layers >= firstShared reuse K/V from donor layers + isShared := false + if donorLayer, ok := m.kvDonorMap[i]; ok { + // Set cache layer to donor so Get() reads donor's K/V + cache.SetLayer(donorLayer) + isShared = true + } + hiddenState = layer.Forward(ctx, i, hiddenState, positions, perLayerInput, lastLayerOutputs, cache, isShared, &m.TextOptions) + } + + return m.OutputNorm.Forward(ctx, hiddenState, m.eps) +} + +// PerLayerProjector implements PLE. +type PerLayerProjector struct { + TokenEmbedding *nn.Embedding `gguf:"per_layer_token_embd"` + Projector *nn.Linear `gguf:"per_layer_model_proj"` + Norm *nn.RMSNorm `gguf:"per_layer_proj_norm"` +} + +func (p *PerLayerProjector) Forward(ctx ml.Context, batch input.Batch, inputs ml.Tensor, opts *TextOptions) ml.Tensor { + inputsPerLayer := p.TokenEmbedding.Forward(ctx, batch.Inputs) + inputsPerLayer = inputsPerLayer.Scale(ctx, math.Sqrt(float64(opts.hiddenSizePerLayerInput))) + // Reshape to [pleDim, numLayers, numTokens] — matching projection shape + inputsPerLayer = inputsPerLayer.Reshape(ctx, opts.hiddenSizePerLayerInput, opts.hiddenLayers, inputs.Dim(1)) + + perLayerProjection := p.Projector.Forward(ctx, inputs) + perLayerProjection = perLayerProjection.Scale(ctx, 1.0/math.Sqrt(float64(opts.hiddenSize))) + perLayerProjection = perLayerProjection.Reshape(ctx, opts.hiddenSizePerLayerInput, opts.hiddenLayers, inputs.Dim(1)) + perLayerProjection = p.Norm.Forward(ctx, perLayerProjection, opts.eps) + + if inputsPerLayer != nil { + perLayerProjection = perLayerProjection.Add(ctx, inputsPerLayer) + perLayerProjection = perLayerProjection.Scale(ctx, 1/math.Sqrt(2)) + } + + return perLayerProjection +} + +type TextSelfAttention struct { + Query *nn.Linear `gguf:"attn_q"` + QueryNorm *nn.RMSNorm `gguf:"attn_q_norm"` + Key *nn.Linear `gguf:"attn_k"` + KeyNorm *nn.RMSNorm `gguf:"attn_k_norm"` + Value *nn.Linear `gguf:"attn_v"` + Output *nn.Linear `gguf:"attn_output"` + RopeFactors ml.Tensor `gguf:"rope_freqs.weight"` // proportional RoPE freq_factors +} + +func (sa *TextSelfAttention) Forward(ctx ml.Context, layer int, hiddenState, positions ml.Tensor, cache kvcache.Cache, sharedKV bool, opts *TextOptions) ml.Tensor { + batchSize := hiddenState.Dim(1) + hd := opts.headDimForLayer(layer) + kvHeads := opts.kvHeadsForLayer(layer) + ropeBase, ropeDims := opts.ropeForLayer(layer) + + q := sa.Query.Forward(ctx, hiddenState) + q = q.Reshape(ctx, hd, opts.numHeads, batchSize) + q = sa.QueryNorm.Forward(ctx, q, opts.eps) + + var k, v ml.Tensor + if !sharedKV { + k = sa.Key.Forward(ctx, hiddenState) + k = k.Reshape(ctx, hd, kvHeads, batchSize) + + if sa.Value != nil { + v = sa.Value.Forward(ctx, hiddenState) + v = v.Reshape(ctx, hd, kvHeads, batchSize) + } else { + // K=V: use raw K projection (before K norm) as V + v = k + } + + k = sa.KeyNorm.Forward(ctx, k, opts.eps) + v = v.RMSNorm(ctx, nil, opts.eps) // V norm: unweighted RMSNorm + } + + // RoPE with proportional freq_factors on global layers + ropeOpts := []func(*rope.Options){rope.WithTypeNeoX()} + if sa.RopeFactors != nil && !opts.isLocal(layer) { + ropeOpts = append(ropeOpts, rope.WithFactors(sa.RopeFactors)) + } + q = nn.RoPE(ctx, q, positions, ropeDims, ropeBase, 1.0, ropeOpts...) + if k != nil { + k = nn.RoPE(ctx, k, positions, ropeDims, ropeBase, 1.0, ropeOpts...) + } + + attention := nn.Attention(ctx, q, k, v, 1.0, cache) + + attention = attention.Reshape(ctx, hd*opts.numHeads, batchSize) + return sa.Output.Forward(ctx, attention) +} + +type TextMLP struct { + Gate *nn.Linear `gguf:"ffn_gate"` + Up *nn.Linear `gguf:"ffn_up"` + Down *nn.Linear `gguf:"ffn_down"` +} + +func (mlp *TextMLP) Forward(ctx ml.Context, hiddenState ml.Tensor) ml.Tensor { + hiddenState = mlp.Gate.Forward(ctx, hiddenState).GELU(ctx, mlp.Up.Forward(ctx, hiddenState)) + return mlp.Down.Forward(ctx, hiddenState) +} + +// TextRouter implements the Gemma 4 MoE router. +type TextRouter struct { + Proj *nn.Linear `gguf:"ffn_gate_inp"` + Scale ml.Tensor `gguf:"ffn_gate_inp.scale"` +} + +func (r *TextRouter) Forward(ctx ml.Context, hiddenState ml.Tensor, opts *TextOptions) (routingWeights, selectedExperts ml.Tensor) { + // RMSNorm without learned weight + x := hiddenState.RMSNorm(ctx, nil, opts.eps) + // Scale by 1/sqrt(hidden_size) + x = x.Scale(ctx, 1.0/math.Sqrt(float64(opts.hiddenSize))) + // Multiply by learned scale parameter + x = x.Mul(ctx, r.Scale) + // Project to expert logits + expertScores := r.Proj.Forward(ctx, x) + // Softmax over experts + routingWeights = expertScores.Softmax(ctx) + // TopK expert selection + selectedExperts = routingWeights.TopK(ctx, opts.numExpertsUsed) + return routingWeights, selectedExperts +} + +// TextMoEBlock implements the Gemma 4 sparse MoE. +type TextMoEBlock struct { + GateUp *nn.LinearBatch `gguf:"ffn_gate_up_exps"` + Gate *nn.LinearBatch `gguf:"ffn_gate_exps"` + Up *nn.LinearBatch `gguf:"ffn_up_exps"` + Down *nn.LinearBatch `gguf:"ffn_down_exps"` + DownScale ml.Tensor `gguf:"ffn_down_exps.scale,alt:ffn_gate_inp.per_expert_scale"` +} + +func (moe *TextMoEBlock) Forward(ctx ml.Context, hiddenState, routingWeights, selectedExperts ml.Tensor, opts *TextOptions) ml.Tensor { + // Select routing weights for chosen experts and renormalize + routingWeights = routingWeights.Reshape(ctx, 1, opts.numExperts, hiddenState.Dim(1)).Rows(ctx, selectedExperts) + routingWeights = routingWeights.Reshape(ctx, opts.numExpertsUsed, hiddenState.Dim(1)) + routingWeights = routingWeights.Div(ctx, routingWeights.SumRows(ctx)) + routingWeights = routingWeights.Reshape(ctx, 1, opts.numExpertsUsed, hiddenState.Dim(1)) + + hiddenState = hiddenState.Reshape(ctx, hiddenState.Dim(0), 1, hiddenState.Dim(1)) + + // Expert computation using LinearBatch (MulmatID selecting experts by index) + var gateOut, upOut ml.Tensor + if moe.GateUp != nil && moe.GateUp.Weight != nil { + gateUp := moe.GateUp.Forward(ctx, hiddenState, selectedExperts) + nFF := gateUp.Dim(0) / 2 + gateOut = gateUp.Slice(ctx, 0, 0, nFF, 1) + upOut = gateUp.Slice(ctx, 0, nFF, gateUp.Dim(0), 1) + } else { + gateOut = moe.Gate.Forward(ctx, hiddenState, selectedExperts) + upOut = moe.Up.Forward(ctx, hiddenState, selectedExperts) + } + hiddenState = gateOut.GELU(ctx, upOut) + experts := moe.Down.Forward(ctx, hiddenState, selectedExperts) + + // Apply per-expert down projection scale when present. + if moe.DownScale != nil { + expertScales := moe.DownScale.Reshape(ctx, opts.numExperts, 1) + expertScales = expertScales.Repeat(ctx, 1, hiddenState.Dim(2)) + expertScales = expertScales.Reshape(ctx, 1, opts.numExperts, hiddenState.Dim(2)).Rows(ctx, selectedExperts) + expertScales = expertScales.Reshape(ctx, opts.numExpertsUsed, hiddenState.Dim(2)) + expertScales = expertScales.Reshape(ctx, 1, opts.numExpertsUsed, hiddenState.Dim(2)) + experts = experts.Mul(ctx, expertScales) + } + + // Apply routing weights + experts = experts.Mul(ctx, routingWeights) + + // Sum across experts + nextStates := experts.View(ctx, 0, experts.Dim(0), experts.Stride(2), experts.Dim(2)) + for i := 1; i < opts.numExpertsUsed; i++ { + nextStates = nextStates.Add(ctx, experts.View(ctx, i*experts.Stride(1), experts.Dim(0), experts.Stride(2), experts.Dim(2))) + } + + return nextStates +} + +type TextLayer struct { + AttentionNorm *nn.RMSNorm `gguf:"attn_norm"` + SelfAttention *TextSelfAttention + PostAttentionNorm *nn.RMSNorm `gguf:"post_attention_norm,alt:attn_post_norm"` + MLPNorm *nn.RMSNorm `gguf:"ffn_norm,alt:ffn_pre_norm"` + MLP *TextMLP + PostMLPNorm *nn.RMSNorm `gguf:"post_ffw_norm,alt:ffn_post_norm"` + + // MoE (present only for models with enable_moe_block=true) + Router *TextRouter + MoE *TextMoEBlock + MoENorm *nn.RMSNorm `gguf:"pre_ffw_norm_2,alt:ffn_pre_norm_2"` + PostMoENorm *nn.RMSNorm `gguf:"post_ffw_norm_2,alt:ffn_post_norm_2"` + PostMLPNorm1 *nn.RMSNorm `gguf:"post_ffw_norm_1,alt:ffn_post_norm_1"` // used instead of PostMLPNorm when MoE is present + + PerLayerInputGate *nn.Linear `gguf:"inp_gate"` + PerLayerProjection *nn.Linear `gguf:"proj"` + PostPerLayerNorm *nn.RMSNorm `gguf:"post_norm"` + LayerScalar ml.Tensor `gguf:"layer_scalar,alt:layer_output_scale.weight"` +} + +func (l *TextLayer) Forward(ctx ml.Context, layer int, hiddenState, positions, perLayerInput, outputs ml.Tensor, cache kvcache.Cache, sharedKV bool, opts *TextOptions) ml.Tensor { + residual := hiddenState + + hiddenState = l.AttentionNorm.Forward(ctx, hiddenState, opts.eps) + hiddenState = l.SelfAttention.Forward(ctx, layer, hiddenState, positions, cache, sharedKV, opts) + hiddenState = l.PostAttentionNorm.Forward(ctx, hiddenState, opts.eps) + + if outputs != nil { + hiddenState = hiddenState.Rows(ctx, outputs) + residual = residual.Rows(ctx, outputs) + if perLayerInput != nil { + perLayerInput = perLayerInput.Rows(ctx, outputs) + } + } + + hiddenState = hiddenState.Add(ctx, residual) + residual = hiddenState + + // MLP (+ optional MoE in parallel) + hasSplitExperts := l.MoE != nil && l.MoE.Gate != nil && l.MoE.Up != nil && l.MoE.Gate.Weight != nil && l.MoE.Up.Weight != nil + hasFusedExperts := l.MoE != nil && l.MoE.GateUp != nil && l.MoE.GateUp.Weight != nil + if l.Router != nil && l.MoE != nil && l.MoE.Down != nil && l.MoE.Down.Weight != nil && (hasSplitExperts || hasFusedExperts) { + // MoE layers: run MLP and MoE in parallel, sum results + mlpState := l.MLPNorm.Forward(ctx, hiddenState, opts.eps) + mlpState = l.MLP.Forward(ctx, mlpState) + mlpState = l.PostMLPNorm1.Forward(ctx, mlpState, opts.eps) + + routingWeights, selectedExperts := l.Router.Forward(ctx, hiddenState, opts) + moeState := l.MoENorm.Forward(ctx, hiddenState, opts.eps) + moeState = l.MoE.Forward(ctx, moeState, routingWeights, selectedExperts, opts) + moeState = l.PostMoENorm.Forward(ctx, moeState, opts.eps) + + // Combine MLP + MoE, apply outer post-FFN norm, then add residual + combined := mlpState.Add(ctx, moeState) + combined = l.PostMLPNorm.Forward(ctx, combined, opts.eps) + hiddenState = combined.Add(ctx, residual) + } else { + // Dense layers: MLP only + hiddenState = l.MLPNorm.Forward(ctx, hiddenState, opts.eps) + hiddenState = l.MLP.Forward(ctx, hiddenState) + hiddenState = l.PostMLPNorm.Forward(ctx, hiddenState, opts.eps) + hiddenState = hiddenState.Add(ctx, residual) + } + + // PLE injection (after MLP residual) + if perLayerInput != nil && l.PerLayerInputGate != nil { + pleState := l.PerLayerInputGate.Forward(ctx, hiddenState) + pleState = pleState.GELU(ctx, perLayerInput) + pleState = l.PerLayerProjection.Forward(ctx, pleState) + pleState = l.PostPerLayerNorm.Forward(ctx, pleState, opts.eps) + hiddenState = hiddenState.Add(ctx, pleState) + } + + // Layer scalar applied at end of layer (full-attention layers only) + if l.LayerScalar != nil { + hiddenState = hiddenState.Mul(ctx, l.LayerScalar) + } + + return hiddenState +} diff --git a/model/models/gemma4/model_vision.go b/model/models/gemma4/model_vision.go new file mode 100644 index 000000000..c0ecccce3 --- /dev/null +++ b/model/models/gemma4/model_vision.go @@ -0,0 +1,384 @@ +package gemma4 + +import ( + "math" + + "github.com/ollama/ollama/fs" + "github.com/ollama/ollama/ml" + "github.com/ollama/ollama/ml/nn" + "github.com/ollama/ollama/ml/nn/rope" +) + +const batchSize = 1 + +// ClippableLinear is a linear layer with optional input/output clamping. +// Required by Gemma4 vision encoder for numerical stability with F16 weights. +type ClippableLinear struct { + Weight ml.Tensor `gguf:"weight"` + + InputMin ml.Tensor `gguf:"input_min"` + InputMax ml.Tensor `gguf:"input_max"` + OutputMin ml.Tensor `gguf:"output_min"` + OutputMax ml.Tensor `gguf:"output_max"` + + inMin, inMax, outMin, outMax float32 + hasClamp bool + clampsLoaded bool +} + +func scalarValue(t ml.Tensor) (float32, bool) { + if t == nil { + return 0, false + } + + data := t.BackendGet() + if len(data) == 0 { + return 0, false + } + + return data[0], true +} + +func (l *ClippableLinear) loadClampFromScalars() { + if l.clampsLoaded { + return + } + l.clampsLoaded = true + + const ( + defaultMin = -math.MaxFloat32 + defaultMax = math.MaxFloat32 + ) + + inMin, hasInMin := scalarValue(l.InputMin) + inMax, hasInMax := scalarValue(l.InputMax) + outMin, hasOutMin := scalarValue(l.OutputMin) + outMax, hasOutMax := scalarValue(l.OutputMax) + + if !(hasInMin || hasInMax || hasOutMin || hasOutMax) { + return + } + + l.hasClamp = true + l.inMin = defaultMin + l.inMax = defaultMax + l.outMin = defaultMin + l.outMax = defaultMax + + if hasInMin { + l.inMin = inMin + } + if hasInMax { + l.inMax = inMax + } + if hasOutMin { + l.outMin = outMin + } + if hasOutMax { + l.outMax = outMax + } +} + +func (l *ClippableLinear) Forward(ctx ml.Context, x ml.Tensor) ml.Tensor { + if l.hasClamp { + x = x.Clamp(ctx, l.inMin, l.inMax) + } + out := l.Weight.Mulmat(ctx, x) + if l.hasClamp { + out = out.Clamp(ctx, l.outMin, l.outMax) + } + return out +} + +// InitClamp distributes packed clamp values from v.clamp_data to ClippableLinear structs. +// If scalar clamp tensors (input_min/max, output_min/max) are present, they are used too. +// Layout: numLayers × 7 linears (q,k,v,out,gate,up,down) × 4 floats (inMin,inMax,outMin,outMax) +// then 4 floats for the projector. +func (m *VisionModel) InitClamp(proj *MultiModalProjector) { + if m.clampInitDone { + return + } + m.clampInitDone = true + + linears := func(l *VisionEncoderLayer) []*ClippableLinear { + return []*ClippableLinear{ + l.SelfAttention.Query, l.SelfAttention.Key, l.SelfAttention.Value, + l.SelfAttention.Output, l.MLP.Gate, l.MLP.Up, l.MLP.Down, + } + } + + for i := range m.Layers { + for _, cl := range linears(&m.Layers[i]) { + if cl != nil { + cl.loadClampFromScalars() + } + } + } + if proj != nil && proj.Projection != nil { + proj.Projection.loadClampFromScalars() + } + + // Load packed clamp data when present (legacy Ollama format). + if m.ClampData == nil { + return + } + + // Read all clamp values from packed F32 tensor + data := m.ClampData.BackendGet() + if len(data) == 0 { + return + } + + // Distribute to layer linears: 7 per layer × 4 values each + for i := range m.Layers { + for li, cl := range linears(&m.Layers[i]) { + if cl == nil { + continue + } + idx := (i*7 + li) * 4 + if idx+3 < len(data) { + cl.inMin = data[idx] + cl.inMax = data[idx+1] + cl.outMin = data[idx+2] + cl.outMax = data[idx+3] + cl.hasClamp = true + } + } + } + + // Projector clamp values (last 4 floats) + if proj != nil && proj.Projection != nil { + projIdx := len(m.Layers) * 7 * 4 + if projIdx+3 < len(data) { + proj.Projection.inMin = data[projIdx] + proj.Projection.inMax = data[projIdx+1] + proj.Projection.outMin = data[projIdx+2] + proj.Projection.outMax = data[projIdx+3] + proj.Projection.hasClamp = true + } + } +} + +type VisionSelfAttention struct { + Query *ClippableLinear `gguf:"attn_q"` + Key *ClippableLinear `gguf:"attn_k"` + Value *ClippableLinear `gguf:"attn_v"` + QueryNorm *nn.RMSNorm `gguf:"attn_q_norm"` + KeyNorm *nn.RMSNorm `gguf:"attn_k_norm"` + Output *ClippableLinear `gguf:"attn_out"` +} + +func (sa *VisionSelfAttention) Forward(ctx ml.Context, hiddenState, posX, posY, attnMask ml.Tensor, opts *VisionModelOptions) ml.Tensor { + numPatches := hiddenState.Dim(1) + headDim := opts.hiddenSize / opts.numHeads + + query := sa.Query.Forward(ctx, hiddenState) + key := sa.Key.Forward(ctx, hiddenState) + value := sa.Value.Forward(ctx, hiddenState) + + query = query.Reshape(ctx, headDim, opts.numHeads, numPatches, batchSize) + key = key.Reshape(ctx, headDim, opts.numHeads, numPatches, batchSize) + value = value.Reshape(ctx, headDim, opts.numHeads, numPatches, batchSize) + + // Q/K norms (Gemma-style: x * (1 + weight) / rms(x)) + query = sa.QueryNorm.Forward(ctx, query, opts.eps) + key = sa.KeyNorm.Forward(ctx, key, opts.eps) + + // V norm (RMSNorm without learned weights) + value = value.RMSNorm(ctx, nil, opts.eps) + + // 2D RoPE: split head dim in half, apply NeoX RoPE with x positions to first half, + // y positions to second half, then concatenate. + halfDim := headDim / 2 + ropeOpts := rope.WithTypeNeoX() + + qFirst := query.View(ctx, 0, halfDim, query.Stride(1), opts.numHeads, query.Stride(2), numPatches) + qFirst = nn.RoPE(ctx, qFirst, posX, halfDim, opts.ropeTheta, 1.0, ropeOpts) + + kFirst := key.View(ctx, 0, halfDim, key.Stride(1), opts.numHeads, key.Stride(2), numPatches) + kFirst = nn.RoPE(ctx, kFirst, posX, halfDim, opts.ropeTheta, 1.0, ropeOpts) + + halfOffset := halfDim * query.Stride(0) + qSecond := query.View(ctx, halfOffset, halfDim, query.Stride(1), opts.numHeads, query.Stride(2), numPatches) + qSecond = nn.RoPE(ctx, qSecond, posY, halfDim, opts.ropeTheta, 1.0, ropeOpts) + + halfOffsetK := halfDim * key.Stride(0) + kSecond := key.View(ctx, halfOffsetK, halfDim, key.Stride(1), opts.numHeads, key.Stride(2), numPatches) + kSecond = nn.RoPE(ctx, kSecond, posY, halfDim, opts.ropeTheta, 1.0, ropeOpts) + + query = qFirst.Concat(ctx, qSecond, 0) + key = kFirst.Concat(ctx, kSecond, 0) + + // Use flash attention for numerical stability (handles large attention scores + // from unclamped RMSNorm weights, e.g. 26B has addOne weights up to 19.5) + attention := nn.Attention(ctx, query, key, value, 1.0, nil) + attention = attention.Reshape(ctx, opts.hiddenSize, attention.Dim(2), batchSize) + + return sa.Output.Forward(ctx, attention) +} + +type VisionMLP struct { + Gate *ClippableLinear `gguf:"ffn_gate"` + Up *ClippableLinear `gguf:"ffn_up"` + Down *ClippableLinear `gguf:"ffn_down"` +} + +func (mlp *VisionMLP) Forward(ctx ml.Context, hiddenState ml.Tensor) ml.Tensor { + gate := mlp.Gate.Forward(ctx, hiddenState) + up := mlp.Up.Forward(ctx, hiddenState) + hiddenState = gate.QuickGELU(ctx, up) + return mlp.Down.Forward(ctx, hiddenState) +} + +type VisionEncoderLayer struct { + AttentionNorm *nn.RMSNorm `gguf:"ln1"` + SelfAttention *VisionSelfAttention + PostAttentionNorm *nn.RMSNorm `gguf:"attn_post_norm"` + + FFNNorm *nn.RMSNorm `gguf:"ln2"` + MLP *VisionMLP + PostFFNNorm *nn.RMSNorm `gguf:"ffn_post_norm"` + + LayerOutputScale ml.Tensor `gguf:"out_scale.weight"` +} + +func (e *VisionEncoderLayer) Forward(ctx ml.Context, hiddenState, posX, posY, attnMask ml.Tensor, opts *VisionModelOptions) ml.Tensor { + residual := hiddenState + + // Pre-attention norm -> self attention -> post-attention norm + hiddenState = e.AttentionNorm.Forward(ctx, hiddenState, opts.eps) + hiddenState = e.SelfAttention.Forward(ctx, hiddenState, posX, posY, attnMask, opts) + hiddenState = e.PostAttentionNorm.Forward(ctx, hiddenState, opts.eps) + + // Residual connection + hiddenState = hiddenState.Add(ctx, residual) + residual = hiddenState + + // Pre-FFN norm -> FFN -> post-FFN norm + hiddenState = e.FFNNorm.Forward(ctx, hiddenState, opts.eps) + hiddenState = e.MLP.Forward(ctx, hiddenState) + hiddenState = e.PostFFNNorm.Forward(ctx, hiddenState, opts.eps) + + // Residual connection + hiddenState = hiddenState.Add(ctx, residual) + + // Per-layer output scale + if e.LayerOutputScale != nil { + hiddenState = hiddenState.Mul(ctx, e.LayerOutputScale) + } + + return hiddenState +} + +type VisionModelOptions struct { + hiddenSize int + numHeads int + patchSize int + nMerge int + eps float32 + ropeTheta float32 +} + +type VisionModel struct { + PatchEmbedding *nn.Conv2D `gguf:"patch_embd"` + PositionEmbedding ml.Tensor `gguf:"position_embd.weight"` + ClampData ml.Tensor `gguf:"clamp_data"` + StdBias ml.Tensor `gguf:"std_bias"` + StdScale ml.Tensor `gguf:"std_scale"` + + Layers []VisionEncoderLayer `gguf:"blk"` + + *VisionModelOptions + clampInitDone bool +} + +func (m *VisionModel) Forward(ctx ml.Context, pixelValues ml.Tensor, numPatchesX, numPatchesY int) ml.Tensor { + numPatches := numPatchesX * numPatchesY + + // Patch embedding via Conv2D + hiddenState := m.PatchEmbedding.Forward(ctx, pixelValues, m.patchSize, m.patchSize, 0, 0, 1, 1) + hiddenState = hiddenState.Reshape(ctx, numPatches, m.hiddenSize) + hiddenState = hiddenState.Permute(ctx, 1, 0, 2, 3).Contiguous(ctx) + + // Conv2D with F16 weights produces F16 output via im2col; cast to F32 for encoder precision + hiddenState = hiddenState.Cast(ctx, ml.DTypeF32) + + // 2D positional embeddings from 3D tensor [nEmbd, maxPos, 2] + posSize := m.PositionEmbedding.Dim(1) + nb1 := m.PositionEmbedding.Stride(1) + tblX := m.PositionEmbedding.View(ctx, 0, m.hiddenSize, nb1, posSize) + tblY := m.PositionEmbedding.View(ctx, posSize*nb1, m.hiddenSize, nb1, posSize) + + // Position indices for patches + posXData := make([]int32, numPatches) + posYData := make([]int32, numPatches) + for i := range numPatches { + posXData[i] = int32(i % numPatchesX) + posYData[i] = int32(i / numPatchesX) + } + + posXEmb := ctx.Input().FromInts(posXData, numPatches) + posYEmb := ctx.Input().FromInts(posYData, numPatches) + + hiddenState = hiddenState.Add(ctx, tblX.Rows(ctx, posXEmb)) + hiddenState = hiddenState.Add(ctx, tblY.Rows(ctx, posYEmb)) + + // No attention mask — all positions are real patches + var attnMask ml.Tensor + + // RoPE positions + posXRope := ctx.Input().FromInts(posXData, numPatches) + posYRope := ctx.Input().FromInts(posYData, numPatches) + + // Vision transformer layers + for i := range m.Layers { + hiddenState = m.Layers[i].Forward(ctx, hiddenState, posXRope, posYRope, attnMask, m.VisionModelOptions) + } + + return hiddenState +} + +func newVisionModel(c fs.Config) *VisionModel { + return &VisionModel{ + Layers: make([]VisionEncoderLayer, c.Uint("vision.block_count")), + VisionModelOptions: &VisionModelOptions{ + hiddenSize: int(c.Uint("vision.embedding_length")), + numHeads: int(c.Uint("vision.attention.head_count")), + patchSize: int(c.Uint("vision.patch_size", 16)), + nMerge: int(c.Uint("vision.projector.scale_factor", 3)), + eps: c.Float("vision.attention.layer_norm_epsilon", 1e-6), + ropeTheta: 100.0, + }, + } +} + +func visionPoolAndProject(ctx ml.Context, hiddenState ml.Tensor, numPatchesX, numPatchesY int, opts *VisionModelOptions, proj *MultiModalProjector, stdBias, stdScale ml.Tensor) ml.Tensor { + hiddenSize := opts.hiddenSize + + // Reshape from [hiddenSize, numPatches] to spatial layout for pooling + hiddenState = hiddenState.Permute(ctx, 1, 0, 2, 3).Contiguous(ctx) + hiddenState = hiddenState.Reshape(ctx, numPatchesX, numPatchesY, hiddenSize) + + // AvgPool2D with kernel=stride=nMerge + hiddenState = hiddenState.AvgPool2D(ctx, opts.nMerge, opts.nMerge, 0) + + // Reshape back to [hiddenSize, numMergedPatches] + mergedX := numPatchesX / opts.nMerge + mergedY := numPatchesY / opts.nMerge + hiddenState = hiddenState.Reshape(ctx, mergedX*mergedY, hiddenSize) + hiddenState = hiddenState.Permute(ctx, 1, 0, 2, 3).Contiguous(ctx) + + hiddenState = hiddenState.Cast(ctx, ml.DTypeF32) + hiddenState = hiddenState.Scale(ctx, math.Sqrt(float64(hiddenSize))) + + // Optional vision standardization before projection. + if stdBias != nil && stdScale != nil { + hiddenState = hiddenState.Sub(ctx, stdBias) + hiddenState = hiddenState.Mul(ctx, stdScale) + } + + // Project to text embedding dimension + hiddenState = proj.Forward(ctx, hiddenState, opts.eps) + + return hiddenState +} diff --git a/model/models/gemma4/process_audio.go b/model/models/gemma4/process_audio.go new file mode 100644 index 000000000..63fdf3512 --- /dev/null +++ b/model/models/gemma4/process_audio.go @@ -0,0 +1,280 @@ +package gemma4 + +import ( + "encoding/binary" + "fmt" + "math" + "math/cmplx" +) + +// Audio preprocessing constants. +const ( + audioSampleRate = 16000 + melBins = 128 + frameLengthMs = 20.0 + hopLengthMs = 10.0 + minFrequency = 0.0 + maxFrequency = 8000.0 + melFloor = 1e-3 + maxAudioSoftTokens = 750 +) + +// Computed from the above constants. +var ( + frameLength = int(math.Round(audioSampleRate * frameLengthMs / 1000.0)) // 320 + hopLength = int(math.Round(audioSampleRate * hopLengthMs / 1000.0)) // 160 +) + +// decodeWAV extracts mono float32 PCM samples from a WAV file, resampled to 16kHz. +func decodeWAV(data []byte) ([]float32, error) { + if len(data) < 12 { + return nil, fmt.Errorf("WAV file too short") + } + if string(data[0:4]) != "RIFF" || string(data[8:12]) != "WAVE" { + return nil, fmt.Errorf("not a WAV file") + } + + var audioFormat uint16 + var numChannels, sampleRate, bitsPerSample int + var audioData []byte + foundFmt := false + + offset := 12 + for offset+8 <= len(data) { + chunkID := string(data[offset : offset+4]) + chunkSize := int(binary.LittleEndian.Uint32(data[offset+4 : offset+8])) + chunkData := data[offset+8 : min(offset+8+chunkSize, len(data))] + + switch chunkID { + case "fmt ": + if len(chunkData) < 16 { + return nil, fmt.Errorf("fmt chunk too short") + } + audioFormat = binary.LittleEndian.Uint16(chunkData[0:2]) + numChannels = int(binary.LittleEndian.Uint16(chunkData[2:4])) + sampleRate = int(binary.LittleEndian.Uint32(chunkData[4:8])) + bitsPerSample = int(binary.LittleEndian.Uint16(chunkData[14:16])) + if audioFormat == 0xFFFE && len(chunkData) >= 26 { + audioFormat = binary.LittleEndian.Uint16(chunkData[24:26]) + } + foundFmt = true + case "data": + audioData = chunkData + } + + offset += 8 + chunkSize + if chunkSize%2 != 0 { + offset++ + } + } + + if !foundFmt { + return nil, fmt.Errorf("no fmt chunk found in WAV file") + } + if audioFormat != 1 && audioFormat != 3 { + return nil, fmt.Errorf("unsupported WAV format: %d (need PCM=1 or float=3)", audioFormat) + } + if audioData == nil { + return nil, fmt.Errorf("no data chunk found in WAV file") + } + + samples := decodeWAVSamples(audioData, audioFormat, bitsPerSample, numChannels) + if sampleRate != audioSampleRate { + samples = resampleLinear(samples, sampleRate, audioSampleRate) + } + return samples, nil +} + +func decodeWAVSamples(data []byte, format uint16, bits, channels int) []float32 { + bytesPerSample := bits / 8 + totalSamples := len(data) / (bytesPerSample * channels) + mono := make([]float32, totalSamples) + + for i := range totalSamples { + var sum float64 + for ch := range channels { + off := (i*channels + ch) * bytesPerSample + if off+bytesPerSample > len(data) { + break + } + switch { + case format == 1 && bits == 16: + v := int16(binary.LittleEndian.Uint16(data[off : off+2])) + sum += float64(v) / 32768.0 + case format == 1 && bits == 32: + v := int32(binary.LittleEndian.Uint32(data[off : off+4])) + sum += float64(v) / 2147483648.0 + case format == 1 && bits == 24: + v := int32(data[off]) | int32(data[off+1])<<8 | int32(data[off+2])<<16 + if v&0x800000 != 0 { + v |= ^0xFFFFFF + } + sum += float64(v) / 8388608.0 + case format == 3 && bits == 32: + v := math.Float32frombits(binary.LittleEndian.Uint32(data[off : off+4])) + sum += float64(v) + case format == 1 && bits == 8: + sum += (float64(data[off]) - 128.0) / 128.0 + } + } + mono[i] = float32(sum / float64(channels)) + } + return mono +} + +func resampleLinear(samples []float32, fromRate, toRate int) []float32 { + n := int(float64(len(samples)) / float64(fromRate) * float64(toRate)) + out := make([]float32, n) + for i := range n { + pos := float64(i) * float64(len(samples)-1) / float64(n-1) + idx := int(pos) + frac := float32(pos - float64(idx)) + if idx+1 < len(samples) { + out[i] = samples[idx]*(1-frac) + samples[idx+1]*frac + } else { + out[i] = samples[idx] + } + } + return out +} + +// computeMelSpectrogram computes the log mel spectrogram from PCM samples. +// Returns shape [numFrames, melBins] as float32 slice, and numFrames. +func computeMelSpectrogram(samples []float32) ([]float32, int) { + fftLen := 1 + for fftLen < frameLength { + fftLen <<= 1 + } + fftLen *= 2 // fft_overdrive=True + + // Hanning-nonzero window. + window := make([]float64, frameLength) + arg := math.Pi * 2.0 / float64(frameLength) + for i := range frameLength { + window[i] = 0.5 - 0.5*math.Cos(arg*(float64(i)+0.5)) + } + + numFreqBins := fftLen/2 + 1 + melFilters := buildMelFilterBank(numFreqBins, melBins, minFrequency, maxFrequency, audioSampleRate) + + frameSizeForUnfold := frameLength + 1 + numFrames := (len(samples) - frameSizeForUnfold) / hopLength + if numFrames <= 0 { + return nil, 0 + } + + result := make([]float32, numFrames*melBins) + fftInput := make([]complex128, fftLen) + + for f := range numFrames { + start := f * hopLength + for i := range frameLength { + fftInput[i] = complex(float64(samples[start+i])*window[i], 0) + } + for i := frameLength; i < fftLen; i++ { + fftInput[i] = 0 + } + + fft(fftInput) + + for m := range melBins { + var melVal float64 + for k := range numFreqBins { + mag := cmplx.Abs(fftInput[k]) + melVal += mag * float64(melFilters[k*melBins+m]) + } + if melVal < melFloor { + melVal = melFloor + } + result[f*melBins+m] = float32(math.Log(melVal)) + } + } + + return result, numFrames +} + +func buildMelFilterBank(numFreqBins, numMels int, fMin, fMax float64, sr int) []float32 { + hzToMel := func(f float64) float64 { + return 2595.0 * math.Log10(1.0+f/700.0) + } + melToHz := func(m float64) float64 { + return 700.0 * (math.Pow(10.0, m/2595.0) - 1.0) + } + + melMin := hzToMel(fMin) + melMax := hzToMel(fMax) + + melPts := make([]float64, numMels+2) + for i := range melPts { + melPts[i] = melMin + float64(i)*(melMax-melMin)/float64(numMels+1) + } + filterFreqs := make([]float64, numMels+2) + for i, m := range melPts { + filterFreqs[i] = melToHz(m) + } + + fftFreqs := make([]float64, numFreqBins) + for i := range fftFreqs { + fftFreqs[i] = float64(i) * float64(sr) / float64(2*(numFreqBins-1)) + } + + filters := make([]float32, numFreqBins*numMels) + for m := range numMels { + fLeft := filterFreqs[m] + fCenter := filterFreqs[m+1] + fRight := filterFreqs[m+2] + for k := range numFreqBins { + f := fftFreqs[k] + var v float64 + if f >= fLeft && f <= fCenter && fCenter > fLeft { + v = (f - fLeft) / (fCenter - fLeft) + } else if f > fCenter && f <= fRight && fRight > fCenter { + v = (fRight - f) / (fRight - fCenter) + } + if v > 0 { + filters[k*numMels+m] = float32(v) + } + } + } + return filters +} + +// fft performs an in-place Cooley-Tukey radix-2 FFT. +func fft(x []complex128) { + n := len(x) + if n <= 1 { + return + } + + j := 0 + for i := 1; i < n; i++ { + bit := n >> 1 + for j&bit != 0 { + j ^= bit + bit >>= 1 + } + j ^= bit + if i < j { + x[i], x[j] = x[j], x[i] + } + } + + for size := 2; size <= n; size <<= 1 { + halfSize := size / 2 + w := complex(math.Cos(2*math.Pi/float64(size)), -math.Sin(2*math.Pi/float64(size))) + for start := 0; start < n; start += size { + wn := complex(1, 0) + for k := range halfSize { + t := wn * x[start+k+halfSize] + x[start+k+halfSize] = x[start+k] - t + x[start+k] = x[start+k] + t + wn *= w + } + } + } +} + +// isAudioData checks if the data starts with WAV magic bytes. +func isAudioData(data []byte) bool { + return len(data) >= 12 && string(data[0:4]) == "RIFF" && string(data[8:12]) == "WAVE" +} diff --git a/model/models/gemma4/process_image.go b/model/models/gemma4/process_image.go new file mode 100644 index 000000000..0404ffb7b --- /dev/null +++ b/model/models/gemma4/process_image.go @@ -0,0 +1,103 @@ +package gemma4 + +import ( + "image" + "math" + + "golang.org/x/image/draw" + + "github.com/ollama/ollama/fs" +) + +type ImageProcessor struct { + patchSize int + numChannels int + nMerge int + minPixels int + maxPixels int +} + +func newImageProcessor(c fs.Config) ImageProcessor { + patchSize := int(c.Uint("vision.patch_size", 16)) + nMerge := int(c.Uint("vision.projector.scale_factor", 3)) + numChannels := int(c.Uint("vision.num_channels", 3)) + + // Token limits from reference: min=40, max=280 output tokens after pooling. + // Convert to pixel counts: tokens * nMerge^2 * patchSize^2 + minTokens := 40 + maxTokens := 280 + patchArea := patchSize * patchSize * nMerge * nMerge + minPixels := minTokens * patchArea + maxPixels := maxTokens * patchArea + + return ImageProcessor{ + patchSize: patchSize, + numChannels: numChannels, + nMerge: nMerge, + minPixels: minPixels, + maxPixels: maxPixels, + } +} + +// ProcessImage resizes an image preserving aspect ratio, aligning dimensions +// to (patchSize * nMerge) boundaries, and normalizes pixels to [-1, 1]. +// Returns the float32 pixel data and the actual output dimensions. +func (p *ImageProcessor) ProcessImage(img image.Image) ([]float32, int, int, error) { + // Compute target size preserving aspect ratio + alignSize := p.patchSize * p.nMerge + targetW, targetH := p.smartResize(img.Bounds().Dx(), img.Bounds().Dy(), alignSize) + + // Resize directly without alpha compositing, matching MLX reference. + dst := image.NewRGBA(image.Rect(0, 0, targetW, targetH)) + draw.BiLinear.Scale(dst, dst.Bounds(), img, img.Bounds(), draw.Over, nil) + + // Normalize to [-1, 1] using mean=0.5, std=0.5: (pixel/255 - 0.5) / 0.5 = 2*pixel/255 - 1 + data := p.pack(dst) + return data, targetW, targetH, nil +} + +// smartResize computes target dimensions that preserve aspect ratio and +// align to alignSize boundaries. It scales the image to fill the maximum +// patch budget (maxPixels), matching the MLX reference. +func (p *ImageProcessor) smartResize(origW, origH, alignSize int) (int, int) { + totalPx := origW * origH + + var targetW, targetH int + if p.maxPixels > 0 && totalPx > 0 { + factor := math.Sqrt(float64(p.maxPixels) / float64(totalPx)) + targetH = max(alignSize, int(math.Floor(factor*float64(origH)/float64(alignSize)))*alignSize) + targetW = max(alignSize, int(math.Floor(factor*float64(origW)/float64(alignSize)))*alignSize) + } else { + targetH = max(alignSize, (origH/alignSize)*alignSize) + targetW = max(alignSize, (origW/alignSize)*alignSize) + } + + return targetW, targetH +} + +// pack extracts RGB values from an image and normalizes to [-1, 1]. +// Returns channel-first layout: [R..., G..., B...]. +func (p *ImageProcessor) pack(img image.Image) []float32 { + bounds := img.Bounds() + w := bounds.Dx() + h := bounds.Dy() + size := w * h + + pixelVals := make([]float32, 3*size) + rOff, gOff, bOff := 0, size, 2*size + + for y := bounds.Min.Y; y < bounds.Max.Y; y++ { + for x := bounds.Min.X; x < bounds.Max.X; x++ { + c := img.At(x, y) + r, g, b, _ := c.RGBA() + idx := (y-bounds.Min.Y)*w + (x - bounds.Min.X) + + // Normalize [0, 255] -> [-1, 1]: 2 * (val/255) - 1 + pixelVals[rOff+idx] = float32(r>>8)/255.0*2.0 - 1.0 + pixelVals[gOff+idx] = float32(g>>8)/255.0*2.0 - 1.0 + pixelVals[bOff+idx] = float32(b>>8)/255.0*2.0 - 1.0 + } + } + + return pixelVals +} diff --git a/model/models/gemma4/tokenizer_compare_test.go b/model/models/gemma4/tokenizer_compare_test.go new file mode 100644 index 000000000..bdb9f5d45 --- /dev/null +++ b/model/models/gemma4/tokenizer_compare_test.go @@ -0,0 +1,102 @@ +package gemma4 + +import ( + "os" + "testing" + + "github.com/ollama/ollama/ml" + "github.com/ollama/ollama/model" + "github.com/ollama/ollama/tokenizer" +) + +// TestTokenizerMatchesHF compares our tokenizer output against HuggingFace reference tokens. +func TestTokenizerMatchesHF(t *testing.T) { + modelPath := os.Getenv("GEMMA4_MODEL_PATH") + if modelPath == "" { + t.Skip("set GEMMA4_MODEL_PATH to a gemma4 GGUF file") + } + + m, err := model.New(modelPath, ml.BackendParams{AllocMemory: true}) + if err != nil { + t.Fatalf("Failed to load model: %v", err) + } + defer m.Backend().Close() + + tok := m.(tokenizer.Tokenizer) + + tests := []struct { + name string + input string + expected []int32 + }{ + { + name: "simple", + input: "Hello, world!", + expected: []int32{9259, 236764, 1902, 236888}, + }, + { + name: "special_tokens", + input: "<|turn>user\nWhat is 2+2?\n<|turn>model\n", + expected: []int32{105, 2364, 107, 3689, 563, 236743, 236778, 236862, 236778, 236881, 106, 107, 105, 4368, 107}, + }, + { + name: "tool_declaration", + input: "<|tool>declaration:bash{description:<|\"|>Run a command<|\"|>}", + expected: []int32{46, 163688, 236787, 42422, 236782, 7777, 236787, 52, 7306, 496, 4991, 52, 236783, 47}, + }, + { + name: "tool_call", + input: "<|tool_call>call:bash{command:<|\"|>ls -la<|\"|>}", + expected: []int32{48, 6639, 236787, 42422, 236782, 7674, 236787, 52, 5629, 753, 2149, 52, 236783, 49}, + }, + { + name: "thinking", + input: "<|channel>thought\nLet me think about this...The answer is 42.", + expected: []int32{100, 45518, 107, 6481, 786, 1751, 1003, 672, 1390, 101, 818, 3890, 563, 236743, 236812, 236778, 236761}, + }, + { + name: "code", + input: "func main() { fmt.Println(\"hello\") }", + expected: []int32{6823, 1689, 825, 642, 22766, 236761, 29006, 885, 23391, 1373, 682}, + }, + { + name: "numbers", + input: "The answer is 42, not 43.5 or -1", + expected: []int32{818, 3890, 563, 236743, 236812, 236778, 236764, 711, 236743, 236812, 236800, 236761, 236810, 653, 753, 236770}, + }, + { + name: "mixed_chat_with_tools", + input: "<|turn>system\nYou are a helpful assistant.\n<|tool>declaration:get_weather{description:<|\"|>Get weather<|\"|>,parameters:{properties:{city:{type:<|\"|>STRING<|\"|>}},type:<|\"|>OBJECT<|\"|>}}\n<|turn>user\nWhat's the weather in Paris?\n<|turn>model\n<|channel>thought\n", + expected: []int32{105, 9731, 107, 3048, 659, 496, 11045, 16326, 236761, 107, 46, 163688, 236787, 828, 236779, 19323, 236782, 7777, 236787, 52, 3407, 7606, 52, 236764, 19031, 29616, 15921, 29616, 13319, 29616, 2084, 236787, 52, 35410, 52, 5237, 2084, 236787, 52, 60688, 52, 1807, 47, 106, 107, 105, 2364, 107, 3689, 236789, 236751, 506, 7606, 528, 9079, 236881, 106, 107, 105, 4368, 107, 100, 45518, 107, 101}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tokens, err := tok.Encode(tt.input, false) // no BOS + if err != nil { + t.Fatalf("encode error: %v", err) + } + + if len(tokens) != len(tt.expected) { + t.Errorf("token count mismatch: got %d, want %d", len(tokens), len(tt.expected)) + t.Logf("got: %v", tokens) + t.Logf("want: %v", tt.expected) + return + } + + mismatches := 0 + for i := range tokens { + if tokens[i] != tt.expected[i] { + mismatches++ + if mismatches <= 5 { + t.Errorf("mismatch at [%d]: got %d, want %d", i, tokens[i], tt.expected[i]) + } + } + } + if mismatches > 5 { + t.Errorf("... and %d more mismatches", mismatches-5) + } + }) + } +} diff --git a/model/models/models.go b/model/models/models.go index 20d9c106c..22439f4a2 100644 --- a/model/models/models.go +++ b/model/models/models.go @@ -7,6 +7,7 @@ import ( _ "github.com/ollama/ollama/model/models/gemma2" _ "github.com/ollama/ollama/model/models/gemma3" _ "github.com/ollama/ollama/model/models/gemma3n" + _ "github.com/ollama/ollama/model/models/gemma4" _ "github.com/ollama/ollama/model/models/glm4moelite" _ "github.com/ollama/ollama/model/models/glmocr" _ "github.com/ollama/ollama/model/models/gptoss" diff --git a/model/parsers/gemma4.go b/model/parsers/gemma4.go new file mode 100644 index 000000000..2701c25f3 --- /dev/null +++ b/model/parsers/gemma4.go @@ -0,0 +1,412 @@ +package parsers + +import ( + "encoding/json" + "errors" + "log/slog" + "strings" + "unicode" + + "github.com/ollama/ollama/api" +) + +type Gemma4ParserState int + +const ( + Gemma4CollectingContent Gemma4ParserState = iota + Gemma4CollectingThinking + Gemma4CollectingToolCall +) + +const ( + gemma4ThinkingOpenTag = "<|channel>" + gemma4ThinkingCloseTag = "" + gemma4ToolCallOpenTag = "<|tool_call>" + gemma4ToolCallCloseTag = "" +) + +type Gemma4Parser struct { + state Gemma4ParserState + buffer strings.Builder + hasThinkingSupport bool + thinkingEnabled bool // true when both model supports and user requested thinking + needsChannelNameStrip bool // true when we just entered thinking and need to strip "thought\n" +} + +func (p *Gemma4Parser) HasToolSupport() bool { + return true +} + +func (p *Gemma4Parser) HasThinkingSupport() bool { + return p.hasThinkingSupport +} + +func (p *Gemma4Parser) Init(tools []api.Tool, lastMessage *api.Message, thinkValue *api.ThinkValue) []api.Tool { + prefill := lastMessage != nil && lastMessage.Role == "assistant" + + p.thinkingEnabled = p.HasThinkingSupport() && (thinkValue != nil && thinkValue.Bool()) + + if !p.thinkingEnabled { + p.state = Gemma4CollectingContent + return tools + } + + if prefill && lastMessage.Content != "" { + p.state = Gemma4CollectingContent + return tools + } + + // When thinking is enabled, start in content mode but we'll switch to + // thinking when we see <|channel>. The model typically starts with + // <|channel> immediately when thinking is enabled. + p.state = Gemma4CollectingContent + return tools +} + +type gemma4Event interface { + isGemma4Event() +} + +type gemma4EventThinkingContent struct { + content string +} + +type gemma4EventContent struct { + content string +} + +type gemma4EventToolCall struct { + toolCall api.ToolCall +} + +func (gemma4EventThinkingContent) isGemma4Event() {} +func (gemma4EventContent) isGemma4Event() {} +func (gemma4EventToolCall) isGemma4Event() {} + +func (p *Gemma4Parser) Add(s string, done bool) (content string, thinking string, calls []api.ToolCall, err error) { + p.buffer.WriteString(s) + events := p.parseEvents(done) + + var toolCalls []api.ToolCall + var contentSb strings.Builder + var thinkingSb strings.Builder + for _, event := range events { + switch event := event.(type) { + case gemma4EventToolCall: + toolCalls = append(toolCalls, event.toolCall) + case gemma4EventThinkingContent: + if p.thinkingEnabled { + thinkingSb.WriteString(event.content) + } + // When thinking is disabled, silently discard channel content + case gemma4EventContent: + contentSb.WriteString(event.content) + } + } + + return contentSb.String(), thinkingSb.String(), toolCalls, nil +} + +func (p *Gemma4Parser) parseEvents(done bool) []gemma4Event { + var all []gemma4Event + + keepLooping := true + for keepLooping { + var events []gemma4Event + events, keepLooping = p.eat(done) + if len(events) > 0 { + all = append(all, events...) + } + } + + return all +} + +// longestOverlap returns the longest overlap between the suffix of bufStr and +// a prefix of any of the given tags. +func longestOverlap(bufStr string, tags ...string) int { + maxOverlap := 0 + for _, tag := range tags { + if o := overlap(bufStr, tag); o > maxOverlap { + maxOverlap = o + } + } + return maxOverlap +} + +func (p *Gemma4Parser) eat(done bool) ([]gemma4Event, bool) { + var events []gemma4Event + bufStr := p.buffer.String() + if bufStr == "" { + return events, false + } + + switch p.state { + case Gemma4CollectingContent: + // Check for thinking open tag + if idx := strings.Index(bufStr, gemma4ThinkingOpenTag); idx != -1 { + contentBefore := bufStr[:idx] + remaining := bufStr[idx+len(gemma4ThinkingOpenTag):] + + p.buffer.Reset() + p.buffer.WriteString(remaining) + p.state = Gemma4CollectingThinking + p.needsChannelNameStrip = true + + if contentBefore = strings.TrimRightFunc(contentBefore, unicode.IsSpace); len(contentBefore) > 0 { + events = append(events, gemma4EventContent{content: contentBefore}) + } + return events, true + } + + // Check for tool call open tag + if idx := strings.Index(bufStr, gemma4ToolCallOpenTag); idx != -1 { + contentBefore := bufStr[:idx] + remaining := bufStr[idx+len(gemma4ToolCallOpenTag):] + + p.buffer.Reset() + p.buffer.WriteString(remaining) + p.state = Gemma4CollectingToolCall + + if contentBefore = strings.TrimRightFunc(contentBefore, unicode.IsSpace); len(contentBefore) > 0 { + events = append(events, gemma4EventContent{content: contentBefore}) + } + return events, true + } + + // Check for partial tag overlap + if !done { + if overlapLen := longestOverlap(bufStr, gemma4ThinkingOpenTag, gemma4ToolCallOpenTag); overlapLen > 0 { + beforePartialTag := bufStr[:len(bufStr)-overlapLen] + trailingLen := trailingWhitespaceLen(beforePartialTag) + ambiguousStart := len(beforePartialTag) - trailingLen + + unambiguous := bufStr[:ambiguousStart] + ambiguous := bufStr[ambiguousStart:] + p.buffer.Reset() + p.buffer.WriteString(ambiguous) + if len(unambiguous) > 0 { + events = append(events, gemma4EventContent{content: unambiguous}) + } + return events, false + } + } + + // No tags found, emit all content + p.buffer.Reset() + if len(bufStr) > 0 { + events = append(events, gemma4EventContent{content: bufStr}) + } + return events, false + + case Gemma4CollectingThinking: + // Strip channel name (e.g., "thought\n") after <|channel>. + // Gemma 4 format: <|channel>thought\n...content... + // In streaming mode, "thought" and "\n" may arrive in separate chunks. + if p.needsChannelNameStrip { + if strings.HasPrefix(bufStr, "thought\n") { + bufStr = bufStr[len("thought\n"):] + p.buffer.Reset() + p.buffer.WriteString(bufStr) + p.needsChannelNameStrip = false + } else if !done && (bufStr == "thought" || strings.HasPrefix("thought\n", bufStr)) { + // Partial match — wait for more data. + return events, false + } else { + // No match (different channel name or no newline) — don't strip. + p.needsChannelNameStrip = false + } + } + + if strings.Contains(bufStr, gemma4ThinkingCloseTag) { + split := strings.SplitN(bufStr, gemma4ThinkingCloseTag, 2) + thinking := strings.TrimRightFunc(split[0], unicode.IsSpace) + remaining := strings.TrimLeftFunc(split[1], unicode.IsSpace) + + p.buffer.Reset() + p.buffer.WriteString(remaining) + p.state = Gemma4CollectingContent + + if len(thinking) > 0 { + events = append(events, gemma4EventThinkingContent{content: thinking}) + } + return events, true + } + + // Check for partial close tag + if !done { + if overlapLen := overlap(bufStr, gemma4ThinkingCloseTag); overlapLen > 0 { + beforePartialTag := bufStr[:len(bufStr)-overlapLen] + trailingLen := trailingWhitespaceLen(beforePartialTag) + ambiguousStart := len(beforePartialTag) - trailingLen + + unambiguous := bufStr[:ambiguousStart] + ambiguous := bufStr[ambiguousStart:] + p.buffer.Reset() + p.buffer.WriteString(ambiguous) + if len(unambiguous) > 0 { + events = append(events, gemma4EventThinkingContent{content: unambiguous}) + } + return events, false + } + } + + // No close tag, emit thinking content (hold back trailing whitespace) + if !done { + whitespaceLen := trailingWhitespaceLen(bufStr) + ambiguousStart := len(bufStr) - whitespaceLen + + unambiguous := bufStr[:ambiguousStart] + ambiguous := bufStr[ambiguousStart:] + p.buffer.Reset() + p.buffer.WriteString(ambiguous) + if len(unambiguous) > 0 { + events = append(events, gemma4EventThinkingContent{content: unambiguous}) + } + } else { + p.buffer.Reset() + if len(bufStr) > 0 { + events = append(events, gemma4EventThinkingContent{content: bufStr}) + } + } + return events, false + + case Gemma4CollectingToolCall: + if idx := strings.Index(bufStr, gemma4ToolCallCloseTag); idx != -1 { + toolCallContent := bufStr[:idx] + remaining := bufStr[idx+len(gemma4ToolCallCloseTag):] + remaining = strings.TrimLeftFunc(remaining, unicode.IsSpace) + + p.buffer.Reset() + p.buffer.WriteString(remaining) + p.state = Gemma4CollectingContent + + if toolCall, err := parseGemma4ToolCall(toolCallContent); err == nil { + events = append(events, gemma4EventToolCall{toolCall: toolCall}) + } else { + slog.Warn("gemma4 tool call parsing failed", "error", err, "content", toolCallContent) + } + return events, true + } + + // If done, flush any accumulated tool call content even without closing tag. + // The model may hit a stop token before emitting . + if done && len(bufStr) > 0 { + p.buffer.Reset() + p.state = Gemma4CollectingContent + if toolCall, err := parseGemma4ToolCall(bufStr); err == nil { + events = append(events, gemma4EventToolCall{toolCall: toolCall}) + } else { + slog.Warn("gemma4 tool call flush on done failed", "error", err, "content", bufStr) + } + return events, false + } + + // Wait for closing tag + return events, false + } + + return events, false +} + +// parseGemma4ToolCall parses a tool call in Gemma 4 format: +// call:NAME{key:value,key:value} +func parseGemma4ToolCall(content string) (api.ToolCall, error) { + // Expected format: call:NAME{args} + if !strings.HasPrefix(content, "call:") { + return api.ToolCall{}, errors.New("expected 'call:' prefix") + } + content = content[len("call:"):] + + // Find the opening brace for args + braceIdx := strings.Index(content, "{") + if braceIdx == -1 { + return api.ToolCall{}, errors.New("expected '{' in tool call") + } + + toolName := strings.TrimSpace(content[:braceIdx]) + argsStr := content[braceIdx:] + + // Convert Gemma 4 argument format to JSON + jsonStr := gemma4ArgsToJSON(argsStr) + + var args api.ToolCallFunctionArguments + if err := json.Unmarshal([]byte(jsonStr), &args); err != nil { + return api.ToolCall{}, err + } + + return api.ToolCall{ + Function: api.ToolCallFunction{ + Name: toolName, + Arguments: args, + }, + }, nil +} + +// gemma4ArgsToJSON converts Gemma 4's custom argument format to valid JSON. +func gemma4ArgsToJSON(s string) string { + s = strings.ReplaceAll(s, `<|"|>`, `"`) + + var buf strings.Builder + buf.Grow(len(s) + 32) + inString := false + hex := "0123456789abcdef" + i := 0 + for i < len(s) { + ch := s[i] + + if ch == '"' { + inString = !inString + buf.WriteByte('"') + i++ + continue + } + + if inString { + switch ch { + case '\\': + buf.WriteString(`\\`) + case '\n': + buf.WriteString(`\n`) + case '\r': + buf.WriteString(`\r`) + case '\t': + buf.WriteString(`\t`) + case '\b': + buf.WriteString(`\b`) + case '\f': + buf.WriteString(`\f`) + default: + if ch < 0x20 { + buf.WriteString(`\u00`) + buf.WriteByte(hex[ch>>4]) + buf.WriteByte(hex[ch&0x0f]) + } else { + buf.WriteByte(ch) + } + } + i++ + continue + } + + if !inString && isIdentStart(ch) { + j := i + 1 + for j < len(s) && isIdentPart(s[j]) { + j++ + } + word := s[i:j] + if j < len(s) && s[j] == ':' { + buf.WriteByte('"') + buf.WriteString(word) + buf.WriteByte('"') + } else { + buf.WriteString(word) + } + i = j + } else { + buf.WriteByte(ch) + i++ + } + } + return buf.String() +} diff --git a/model/parsers/gemma4_test.go b/model/parsers/gemma4_test.go new file mode 100644 index 000000000..9c48f286f --- /dev/null +++ b/model/parsers/gemma4_test.go @@ -0,0 +1,463 @@ +package parsers + +import ( + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/ollama/ollama/api" +) + +func TestGemma4Parser(t *testing.T) { + tests := []struct { + name string + input string + expectedContent string + expectedThinking string + expectedToolCalls []api.ToolCall + thinkingEnabled bool + lastMessage *api.Message + }{ + { + name: "simple_content", + input: "This is a simple response.", + expectedContent: "This is a simple response.", + }, + { + name: "thinking_then_content", + input: "<|channel>thought\nLet me think about this...The answer is 42.", + expectedContent: "The answer is 42.", + expectedThinking: "Let me think about this...", + thinkingEnabled: true, + }, + { + name: "multiple_thinking_blocks", + input: "<|channel>first thought<|channel>second thoughtFinal answer.", + expectedContent: "Final answer.", + expectedThinking: "first thoughtsecond thought", + thinkingEnabled: true, + }, + { + name: "thinking_only_no_content", + input: "<|channel>just thinking", + expectedContent: "", + expectedThinking: "just thinking", + thinkingEnabled: true, + }, + { + name: "tool_call_simple", + input: `<|tool_call>call:get_weather{location:<|"|>Paris<|"|>}`, + expectedToolCalls: []api.ToolCall{ + { + Function: api.ToolCallFunction{ + Name: "get_weather", + Arguments: testArgs(map[string]any{ + "location": "Paris", + }), + }, + }, + }, + }, + { + name: "tool_call_with_multiple_args", + input: `<|tool_call>call:get_weather{location:<|"|>Paris<|"|>,units:<|"|>metric<|"|>}`, + expectedToolCalls: []api.ToolCall{ + { + Function: api.ToolCallFunction{ + Name: "get_weather", + Arguments: testArgs(map[string]any{ + "location": "Paris", + "units": "metric", + }), + }, + }, + }, + }, + { + name: "tool_call_with_number_arg", + input: `<|tool_call>call:set_temp{value:42}`, + expectedToolCalls: []api.ToolCall{ + { + Function: api.ToolCallFunction{ + Name: "set_temp", + Arguments: testArgs(map[string]any{ + "value": 42.0, + }), + }, + }, + }, + }, + { + name: "tool_call_with_boolean_arg", + input: `<|tool_call>call:toggle{enabled:true}`, + expectedToolCalls: []api.ToolCall{ + { + Function: api.ToolCallFunction{ + Name: "toggle", + Arguments: testArgs(map[string]any{ + "enabled": true, + }), + }, + }, + }, + }, + { + name: "tool_call_with_nested_object", + input: `<|tool_call>call:process{config:{enabled:true,name:<|"|>test<|"|>}}`, + expectedToolCalls: []api.ToolCall{ + { + Function: api.ToolCallFunction{ + Name: "process", + Arguments: testArgs(map[string]any{ + "config": map[string]any{ + "enabled": true, + "name": "test", + }, + }), + }, + }, + }, + }, + { + name: "tool_call_with_array", + input: `<|tool_call>call:process{items:[<|"|>a<|"|>,<|"|>b<|"|>]}`, + expectedToolCalls: []api.ToolCall{ + { + Function: api.ToolCallFunction{ + Name: "process", + Arguments: testArgs(map[string]any{ + "items": []any{"a", "b"}, + }), + }, + }, + }, + }, + { + name: "tool_call_with_multiline_string_arg", + input: `<|tool_call>call:bash{command:<|"|>date +<|"|>}`, + expectedToolCalls: []api.ToolCall{ + { + Function: api.ToolCallFunction{ + Name: "bash", + Arguments: testArgs(map[string]any{ + "command": "date\n", + }), + }, + }, + }, + }, + { + name: "multiple_tool_calls", + input: `<|tool_call>call:get_weather{location:<|"|>Paris<|"|>}<|tool_call>call:get_weather{location:<|"|>London<|"|>}`, + expectedToolCalls: []api.ToolCall{ + { + Function: api.ToolCallFunction{ + Name: "get_weather", + Arguments: testArgs(map[string]any{ + "location": "Paris", + }), + }, + }, + { + Function: api.ToolCallFunction{ + Name: "get_weather", + Arguments: testArgs(map[string]any{ + "location": "London", + }), + }, + }, + }, + }, + { + name: "thinking_then_tool_call", + input: "<|channel>thought\nI need to check the weather<|tool_call>call:get_weather{location:<|\"|>Paris<|\"|>}", + expectedThinking: "I need to check the weather", + expectedToolCalls: []api.ToolCall{ + { + Function: api.ToolCallFunction{ + Name: "get_weather", + Arguments: testArgs(map[string]any{ + "location": "Paris", + }), + }, + }, + }, + thinkingEnabled: true, + }, + { + name: "content_then_tool_call", + input: `Let me check that for you.<|tool_call>call:get_weather{location:<|"|>Paris<|"|>}`, + expectedContent: "Let me check that for you.", + expectedToolCalls: []api.ToolCall{ + { + Function: api.ToolCallFunction{ + Name: "get_weather", + Arguments: testArgs(map[string]any{ + "location": "Paris", + }), + }, + }, + }, + }, + { + name: "thinking_disabled_channel_tags_as_content", + input: "<|channel>this is not thinkingactual content", + expectedContent: "actual content", + thinkingEnabled: false, + }, + { + name: "prefill_content_only", + input: "Continuing content.", + expectedContent: "Continuing content.", + lastMessage: &api.Message{ + Role: "assistant", + Content: "Previous content", + }, + thinkingEnabled: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + parser := &Gemma4Parser{hasThinkingSupport: true} + parser.Init(nil, tt.lastMessage, &api.ThinkValue{Value: tt.thinkingEnabled}) + + content, thinking, toolCalls, err := parser.Add(tt.input, true) + if err != nil { + t.Fatalf("Add() error = %v", err) + } + + if diff := cmp.Diff(tt.expectedContent, content); diff != "" { + t.Errorf("content mismatch (-want +got):\n%s", diff) + } + + if diff := cmp.Diff(tt.expectedThinking, thinking); diff != "" { + t.Errorf("thinking mismatch (-want +got):\n%s", diff) + } + + if diff := cmp.Diff(tt.expectedToolCalls, toolCalls, argsComparer); diff != "" { + t.Errorf("tool calls mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestGemma4Parser_Streaming(t *testing.T) { + parser := &Gemma4Parser{hasThinkingSupport: true} + parser.Init(nil, nil, &api.ThinkValue{Value: true}) + + chunks := []string{ + "<|channel>thought", + "\nLet me think", + "...The answer", + " is 42.", + } + + var finalContent, finalThinking strings.Builder + + for i, chunk := range chunks { + done := i == len(chunks)-1 + content, thinking, _, err := parser.Add(chunk, done) + if err != nil { + t.Fatalf("Add() error on chunk %d: %v", i, err) + } + + finalContent.WriteString(content) + finalThinking.WriteString(thinking) + } + + if finalContent.String() != "The answer is 42." { + t.Errorf("expected content %q, got %q", "The answer is 42.", finalContent.String()) + } + + if finalThinking.String() != "Let me think..." { + t.Errorf("expected thinking %q, got %q", "Let me think...", finalThinking.String()) + } +} + +func TestGemma4Parser_StreamingToolCall(t *testing.T) { + parser := &Gemma4Parser{hasThinkingSupport: false} + parser.Init(nil, nil, nil) + + chunks := []string{ + `<|tool_call>call:get_`, + `weather{location:<|"|>Par`, + `is<|"|>}`, + } + + var finalContent strings.Builder + var finalToolCalls []api.ToolCall + + for i, chunk := range chunks { + done := i == len(chunks)-1 + content, _, toolCalls, err := parser.Add(chunk, done) + if err != nil { + t.Fatalf("Add() error on chunk %d: %v", i, err) + } + + finalContent.WriteString(content) + finalToolCalls = append(finalToolCalls, toolCalls...) + } + + if finalContent.String() != "" { + t.Errorf("expected no content, got %q", finalContent.String()) + } + + expectedToolCalls := []api.ToolCall{ + { + Function: api.ToolCallFunction{ + Name: "get_weather", + Arguments: testArgs(map[string]any{ + "location": "Paris", + }), + }, + }, + } + + if diff := cmp.Diff(expectedToolCalls, finalToolCalls, argsComparer); diff != "" { + t.Errorf("tool calls mismatch (-want +got):\n%s", diff) + } +} + +func TestGemma4Parser_StreamingSplitThinkingTag(t *testing.T) { + tests := []struct { + name string + chunks []string + expectedContent string + expectedThinking string + }{ + { + name: "split_channel_open_tag", + chunks: []string{ + "<|chan", + "nel>thinking herecontent", + }, + expectedContent: "content", + expectedThinking: "thinking here", + }, + { + name: "split_channel_close_tag", + chunks: []string{ + "<|channel>thinking herecontent", + }, + expectedContent: "content", + expectedThinking: "thinking here", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + parser := &Gemma4Parser{hasThinkingSupport: true} + parser.Init(nil, nil, &api.ThinkValue{Value: true}) + + var finalContent, finalThinking strings.Builder + for i, chunk := range tt.chunks { + done := i == len(tt.chunks)-1 + content, thinking, _, err := parser.Add(chunk, done) + if err != nil { + t.Fatalf("Add() error on chunk %d: %v", i, err) + } + finalContent.WriteString(content) + finalThinking.WriteString(thinking) + } + + if finalContent.String() != tt.expectedContent { + t.Errorf("expected content %q, got %q", tt.expectedContent, finalContent.String()) + } + if finalThinking.String() != tt.expectedThinking { + t.Errorf("expected thinking %q, got %q", tt.expectedThinking, finalThinking.String()) + } + }) + } +} + +func TestGemma4ArgsToJSON(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "simple_string", + input: `{location:<|"|>Paris<|"|>}`, + expected: `{"location":"Paris"}`, + }, + { + name: "multiple_args", + input: `{location:<|"|>Paris<|"|>,units:<|"|>metric<|"|>}`, + expected: `{"location":"Paris","units":"metric"}`, + }, + { + name: "number_value", + input: `{value:42}`, + expected: `{"value":42}`, + }, + { + name: "boolean_value", + input: `{enabled:true}`, + expected: `{"enabled":true}`, + }, + { + name: "nested_object", + input: `{config:{enabled:true,name:<|"|>test<|"|>}}`, + expected: `{"config":{"enabled":true,"name":"test"}}`, + }, + { + name: "array_value", + input: `{items:[<|"|>a<|"|>,<|"|>b<|"|>]}`, + expected: `{"items":["a","b"]}`, + }, + { + name: "empty_object", + input: `{}`, + expected: `{}`, + }, + { + name: "mixed_types", + input: `{name:<|"|>test<|"|>,count:5,active:true,tags:[<|"|>a<|"|>]}`, + expected: `{"name":"test","count":5,"active":true,"tags":["a"]}`, + }, + { + name: "null_value", + input: `{value:null}`, + expected: `{"value":null}`, + }, + { + name: "multiline_string_value", + input: `{command:<|"|>date +<|"|>}`, + expected: `{"command":"date\n"}`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := gemma4ArgsToJSON(tt.input) + if result != tt.expected { + t.Errorf("expected %q, got %q", tt.expected, result) + } + }) + } +} + +func TestGemma4Parser_HasToolSupport(t *testing.T) { + parser := &Gemma4Parser{} + if !parser.HasToolSupport() { + t.Error("Gemma4Parser should support tools") + } +} + +func TestGemma4Parser_HasThinkingSupport(t *testing.T) { + parser := &Gemma4Parser{hasThinkingSupport: true} + if !parser.HasThinkingSupport() { + t.Error("Gemma4Parser with thinking support should report it") + } + + parser2 := &Gemma4Parser{hasThinkingSupport: false} + if parser2.HasThinkingSupport() { + t.Error("Gemma4Parser without thinking support should not report it") + } +} diff --git a/model/parsers/parsers.go b/model/parsers/parsers.go index ec26f9bbc..38cad3e79 100644 --- a/model/parsers/parsers.go +++ b/model/parsers/parsers.go @@ -77,6 +77,10 @@ func ParserForName(name string) Parser { return &FunctionGemmaParser{} case "glm-4.7": return &GLM47Parser{} + case "gemma4": + return &Gemma4Parser{hasThinkingSupport: true} + case "gemma4-no-thinking": + return &Gemma4Parser{hasThinkingSupport: false} case "glm-ocr": return &GlmOcrParser{} case "lfm2": diff --git a/model/renderers/gemma4.go b/model/renderers/gemma4.go new file mode 100644 index 000000000..ef3d5348d --- /dev/null +++ b/model/renderers/gemma4.go @@ -0,0 +1,379 @@ +package renderers + +import ( + "fmt" + "sort" + "strings" + + "github.com/ollama/ollama/api" +) + +// Gemma4Renderer renders prompts using Gemma 4's chat format with +// <|turn>/ markers, <|"|> string delimiters, and <|tool>/ +// <|tool_call>/<|tool_response> tags for function calling. +type Gemma4Renderer struct { + useImgTags bool +} + +const ( + g4Q = `<|"|>` // Gemma 4 string delimiter +) + +func (r *Gemma4Renderer) Render(messages []api.Message, tools []api.Tool, thinkValue *api.ThinkValue) (string, error) { + var sb strings.Builder + imageOffset := 0 + + // BOS token — Gemma 4 models have add_bos_token=false in their tokenizer + // config, so the tokenizer does not auto-prepend BOS. We must emit it + // explicitly in the rendered prompt, matching the HF chat template. + sb.WriteString("") + // Extract system message if present. + var systemMessage string + var loopMessages []api.Message + hasSystemRole := len(messages) > 0 && (messages[0].Role == "system" || messages[0].Role == "developer") + if hasSystemRole { + systemMessage = messages[0].Content + loopMessages = messages[1:] + } else { + loopMessages = messages + } + + // Emit system turn if there's a system/developer role, tools, or thinking. + hasThink := thinkValue != nil && thinkValue.Bool() + if hasSystemRole || len(tools) > 0 || hasThink { + sb.WriteString("<|turn>system\n") + if hasThink { + sb.WriteString("<|think|>") + } + if systemMessage != "" { + sb.WriteString(strings.TrimSpace(systemMessage)) + } + for _, tool := range tools { + sb.WriteString(r.renderToolDeclaration(tool)) + } + sb.WriteString("\n") + } + + // Each message gets its own <|turn>role\n ... \n block, + // matching the HF chat template exactly. + for _, message := range loopMessages { + switch message.Role { + case "user": + sb.WriteString("<|turn>user\n") + r.renderContent(&sb, message, &imageOffset, true) + sb.WriteString("\n") + + case "assistant": + sb.WriteString("<|turn>model\n") + // Tool calls come before content (matching HF template order) + for _, tc := range message.ToolCalls { + sb.WriteString(r.formatToolCall(tc)) + } + // Strip thinking from history (matching HF strip_thinking macro) + if message.Content != "" { + sb.WriteString(stripThinking(message.Content)) + } + sb.WriteString("\n") + + case "tool": + sb.WriteString("<|turn>tool\n") + sb.WriteString(strings.TrimSpace(message.Content)) + sb.WriteString("\n") + + default: + sb.WriteString("<|turn>" + message.Role + "\n") + sb.WriteString(strings.TrimSpace(message.Content)) + sb.WriteString("\n") + } + } + + // Generation prompt + sb.WriteString("<|turn>model\n") + + return sb.String(), nil +} + +// stripThinking removes <|channel>... thinking blocks from content, +// matching the HF chat template's strip_thinking macro. +func stripThinking(text string) string { + var result strings.Builder + for { + start := strings.Index(text, "<|channel>") + if start == -1 { + result.WriteString(text) + break + } + result.WriteString(text[:start]) + end := strings.Index(text[start:], "") + if end == -1 { + break + } + text = text[start+end+len(""):] + } + return strings.TrimSpace(result.String()) +} + +// renderContent writes a message's content, interleaving [img-N] tags for images. +// When trim is true, leading/trailing whitespace is stripped (matching the Jinja2 +// template's | trim filter applied to non-model content). +func (r *Gemma4Renderer) renderContent(sb *strings.Builder, msg api.Message, imageOffset *int, trim bool) { + if len(msg.Images) > 0 && r.useImgTags { + for range msg.Images { + sb.WriteString(fmt.Sprintf("[img-%d]", *imageOffset)) + *imageOffset++ + } + } + content := msg.Content + if trim { + content = strings.TrimSpace(content) + } + sb.WriteString(content) +} + +func (r *Gemma4Renderer) renderToolDeclaration(tool api.Tool) string { + var sb strings.Builder + fn := tool.Function + + sb.WriteString("<|tool>declaration:" + fn.Name + "{") + sb.WriteString("description:" + g4Q + fn.Description + g4Q) + + if fn.Parameters.Properties != nil || fn.Parameters.Type != "" { + sb.WriteString(",parameters:{") + + needsComma := false + + if fn.Parameters.Properties != nil && fn.Parameters.Properties.Len() > 0 { + sb.WriteString("properties:{") + r.writeProperties(&sb, fn.Parameters.Properties) + sb.WriteString("}") + needsComma = true + } + + if len(fn.Parameters.Required) > 0 { + if needsComma { + sb.WriteString(",") + } + sb.WriteString("required:[") + for i, req := range fn.Parameters.Required { + if i > 0 { + sb.WriteString(",") + } + sb.WriteString(g4Q + req + g4Q) + } + sb.WriteString("]") + needsComma = true + } + + if fn.Parameters.Type != "" { + if needsComma { + sb.WriteString(",") + } + sb.WriteString("type:" + g4Q + strings.ToUpper(fn.Parameters.Type) + g4Q) + } + + sb.WriteString("}") + } + + sb.WriteString("}") + return sb.String() +} + +func (r *Gemma4Renderer) writeProperties(sb *strings.Builder, props *api.ToolPropertiesMap) { + keys := make([]string, 0, props.Len()) + for k := range props.All() { + keys = append(keys, k) + } + sort.Strings(keys) + + first := true + for _, name := range keys { + prop, _ := props.Get(name) + if !first { + sb.WriteString(",") + } + first = false + + sb.WriteString(name + ":{") + + hasContent := false + if prop.Description != "" { + sb.WriteString("description:" + g4Q + prop.Description + g4Q) + hasContent = true + } + + if len(prop.Type) > 0 { + typeName := strings.ToUpper(prop.Type[0]) + + switch typeName { + case "STRING": + if len(prop.Enum) > 0 { + if hasContent { + sb.WriteString(",") + } + sb.WriteString("enum:[") + for j, e := range prop.Enum { + if j > 0 { + sb.WriteString(",") + } + sb.WriteString(g4Q + fmt.Sprintf("%v", e) + g4Q) + } + sb.WriteString("]") + hasContent = true + } + + case "OBJECT": + // Render nested properties recursively. + // Note: the leading comma is hardcoded (matching the template), + // and this does NOT set hasContent — the comma before type: + // depends only on whether description was present. + sb.WriteString(",properties:{") + if prop.Properties != nil && prop.Properties.Len() > 0 { + r.writeProperties(sb, prop.Properties) + } + sb.WriteString("}") + if len(prop.Required) > 0 { + sb.WriteString(",required:[") + for j, req := range prop.Required { + if j > 0 { + sb.WriteString(",") + } + sb.WriteString(g4Q + req + g4Q) + } + sb.WriteString("]") + } + + case "ARRAY": + // Render items specification. + // Same as OBJECT: leading comma is hardcoded, does NOT set hasContent. + if items, ok := prop.Items.(map[string]any); ok && len(items) > 0 { + sb.WriteString(",items:{") + r.writeItemsSpec(sb, items) + sb.WriteString("}") + } + } + + if hasContent { + sb.WriteString(",") + } + sb.WriteString("type:" + g4Q + typeName + g4Q) + } + + sb.WriteString("}") + } +} + +// writeItemsSpec renders the items specification for array-type properties, +// matching the Jinja2 template's dictsort iteration over items. +func (r *Gemma4Renderer) writeItemsSpec(sb *strings.Builder, items map[string]any) { + keys := make([]string, 0, len(items)) + for k := range items { + keys = append(keys, k) + } + sort.Strings(keys) + + first := true + for _, key := range keys { + value := items[key] + if value == nil { + continue + } + if !first { + sb.WriteString(",") + } + first = false + + switch key { + case "type": + if s, ok := value.(string); ok { + sb.WriteString("type:" + g4Q + strings.ToUpper(s) + g4Q) + } + default: + sb.WriteString(key + ":" + r.formatArgValue(value)) + } + } +} + +func (r *Gemma4Renderer) formatToolCall(tc api.ToolCall) string { + var sb strings.Builder + sb.WriteString("<|tool_call>call:" + tc.Function.Name + "{") + + keys := make([]string, 0, tc.Function.Arguments.Len()) + for k := range tc.Function.Arguments.All() { + keys = append(keys, k) + } + sort.Strings(keys) + + first := true + for _, key := range keys { + value, _ := tc.Function.Arguments.Get(key) + if !first { + sb.WriteString(",") + } + first = false + sb.WriteString(key + ":" + r.formatArgValue(value)) + } + + sb.WriteString("}") + return sb.String() +} + +func (r *Gemma4Renderer) formatArgValue(value any) string { + switch v := value.(type) { + case string: + return g4Q + v + g4Q + case bool: + if v { + return "true" + } + return "false" + case float64: + if v == float64(int64(v)) { + return fmt.Sprintf("%d", int64(v)) + } + return fmt.Sprintf("%v", v) + case int, int64, int32: + return fmt.Sprintf("%d", v) + case map[string]any: + return r.formatMapValue(v) + case []any: + return r.formatArrayValue(v) + default: + return fmt.Sprintf("%v", v) + } +} + +func (r *Gemma4Renderer) formatMapValue(m map[string]any) string { + var sb strings.Builder + sb.WriteString("{") + + keys := make([]string, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + sort.Strings(keys) + + first := true + for _, key := range keys { + if !first { + sb.WriteString(",") + } + first = false + sb.WriteString(key + ":" + r.formatArgValue(m[key])) + } + + sb.WriteString("}") + return sb.String() +} + +func (r *Gemma4Renderer) formatArrayValue(arr []any) string { + var sb strings.Builder + sb.WriteString("[") + for i, item := range arr { + if i > 0 { + sb.WriteString(",") + } + sb.WriteString(r.formatArgValue(item)) + } + sb.WriteString("]") + return sb.String() +} diff --git a/model/renderers/gemma4_reference_test.go b/model/renderers/gemma4_reference_test.go new file mode 100644 index 000000000..ecf24dd0f --- /dev/null +++ b/model/renderers/gemma4_reference_test.go @@ -0,0 +1,1274 @@ +package renderers + +// TestGemma4RendererMatchesReference verifies our renderer matches the HF +// Jinja2 chat template exactly. +// +// To regenerate expected values, save gemma4Jinja2Template (below) to +// gemma4_chat_template.jinja2 and run: +// +// python3 -c " +// from jinja2 import Environment; import json +// tmpl = Environment().from_string(open('gemma4_chat_template.jinja2').read()) +// msgs = [{'role':'user','content':'Hello'}] +// print(repr(tmpl.render(messages=msgs, bos_token='', add_generation_prompt=True))) +// " + +import ( + "encoding/json" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + + "github.com/ollama/ollama/api" + "github.com/stretchr/testify/assert" +) + +// The full Jinja2 template is committed as testdata/gemma4_chat_template.jinja2. +// Run with VERIFY_JINJA2=1 to verify expected values against the template using Python. + +func bashRefTool() []api.Tool { + return []api.Tool{{ + Type: "function", + Function: api.ToolFunction{ + Name: "bash", + Description: "Run a command", + Parameters: api.ToolFunctionParameters{ + Type: "object", + Required: []string{"command"}, + Properties: testPropsMap(map[string]api.ToolProperty{ + "command": {Type: api.PropertyType{"string"}, Description: "The command"}, + }), + }, + }, + }} +} + +func bashAndReadRefTools() []api.Tool { + return []api.Tool{ + bashRefTool()[0], + { + Type: "function", + Function: api.ToolFunction{ + Name: "read", + Description: "Read a file", + Parameters: api.ToolFunctionParameters{ + Type: "object", + Required: []string{"path"}, + Properties: testPropsMap(map[string]api.ToolProperty{ + "path": {Type: api.PropertyType{"string"}, Description: "File path"}, + }), + }, + }, + }, + } +} + +func weatherTool() []api.Tool { + return []api.Tool{{ + Type: "function", + Function: api.ToolFunction{ + Name: "get_weather", + Description: "Get weather", + Parameters: api.ToolFunctionParameters{ + Type: "object", + Properties: testPropsMap(map[string]api.ToolProperty{ + "city": {Type: api.PropertyType{"string"}, Description: "City"}, + }), + }, + }, + }} +} + +func addTool() []api.Tool { + return []api.Tool{{ + Type: "function", + Function: api.ToolFunction{ + Name: "add", + Description: "Add numbers", + Parameters: api.ToolFunctionParameters{ + Type: "object", + Properties: testPropsMap(map[string]api.ToolProperty{ + "a": {Type: api.PropertyType{"number"}}, + "b": {Type: api.PropertyType{"number"}}, + }), + }, + }, + }} +} + +func flagTool() []api.Tool { + return []api.Tool{{ + Type: "function", + Function: api.ToolFunction{ + Name: "set_flag", + Description: "Set a flag", + Parameters: api.ToolFunctionParameters{ + Type: "object", + Properties: testPropsMap(map[string]api.ToolProperty{ + "enabled": {Type: api.PropertyType{"boolean"}, Description: "Flag value"}, + }), + }, + }, + }} +} + +func modeTool() []api.Tool { + return []api.Tool{{ + Type: "function", + Function: api.ToolFunction{ + Name: "set_mode", + Description: "Set mode", + Parameters: api.ToolFunctionParameters{ + Type: "object", + Properties: testPropsMap(map[string]api.ToolProperty{ + "mode": {Type: api.PropertyType{"string"}, Description: "The mode", Enum: []any{"fast", "slow"}}, + }), + }, + }, + }} +} + +func bashSmallTool() []api.Tool { + return []api.Tool{{ + Type: "function", + Function: api.ToolFunction{ + Name: "bash", + Description: "Run", + Parameters: api.ToolFunctionParameters{ + Type: "object", + Required: []string{"command"}, + Properties: testPropsMap(map[string]api.ToolProperty{ + "command": {Type: api.PropertyType{"string"}, Description: "Cmd"}, + }), + }, + }, + }} +} + +func nestedTool() []api.Tool { + return []api.Tool{{ + Type: "function", + Function: api.ToolFunction{ + Name: "create", + Description: "Create item", + Parameters: api.ToolFunctionParameters{ + Type: "object", + Properties: testPropsMap(map[string]api.ToolProperty{ + "name": {Type: api.PropertyType{"string"}, Description: "Name"}, + "config": {Type: api.PropertyType{"object"}, Description: "Config", Properties: testPropsMap(map[string]api.ToolProperty{ + "enabled": {Type: api.PropertyType{"boolean"}, Description: "On/off"}, + })}, + }), + }, + }, + }} +} + +func arrayTool() []api.Tool { + return []api.Tool{{ + Type: "function", + Function: api.ToolFunction{ + Name: "batch", + Description: "Run batch", + Parameters: api.ToolFunctionParameters{ + Type: "object", + Properties: testPropsMap(map[string]api.ToolProperty{ + "commands": {Type: api.PropertyType{"array"}, Description: "Commands", Items: map[string]any{"type": "string"}}, + }), + }, + }, + }} +} + +func configureTool() []api.Tool { + return []api.Tool{{ + Type: "function", + Function: api.ToolFunction{ + Name: "configure", + Description: "Configure", + Parameters: api.ToolFunctionParameters{ + Type: "object", + Properties: testPropsMap(map[string]api.ToolProperty{ + "config": {Type: api.PropertyType{"object"}, Description: "Config"}, + }), + }, + }, + }} +} + +func batchArrayTool() []api.Tool { + return []api.Tool{{ + Type: "function", + Function: api.ToolFunction{ + Name: "batch", + Description: "Run batch", + Parameters: api.ToolFunctionParameters{ + Type: "object", + Properties: testPropsMap(map[string]api.ToolProperty{ + "ids": {Type: api.PropertyType{"array"}, Description: "IDs"}, + }), + }, + }, + }} +} + +func countTool() []api.Tool { + return []api.Tool{{ + Type: "function", + Function: api.ToolFunction{ + Name: "count", + Description: "Count items", + Parameters: api.ToolFunctionParameters{ + Type: "object", + Properties: testPropsMap(map[string]api.ToolProperty{ + "n": {Type: api.PropertyType{"number"}}, + }), + }, + }, + }} +} + +func enumNoDescTool() []api.Tool { + return []api.Tool{{ + Type: "function", + Function: api.ToolFunction{ + Name: "set_level", + Description: "Set level", + Parameters: api.ToolFunctionParameters{ + Type: "object", + Properties: testPropsMap(map[string]api.ToolProperty{ + "level": {Type: api.PropertyType{"string"}, Enum: []any{"low", "high"}}, + }), + }, + }, + }} +} + +func nestedRequiredTool() []api.Tool { + return []api.Tool{{ + Type: "function", + Function: api.ToolFunction{ + Name: "create_user", + Description: "Create user", + Parameters: api.ToolFunctionParameters{ + Type: "object", + Properties: testPropsMap(map[string]api.ToolProperty{ + "profile": { + Type: api.PropertyType{"object"}, Description: "Profile", + Required: []string{"name"}, + Properties: testPropsMap(map[string]api.ToolProperty{ + "name": {Type: api.PropertyType{"string"}, Description: "Name"}, + "age": {Type: api.PropertyType{"number"}, Description: "Age"}, + }), + }, + }), + }, + }, + }} +} + +func calcTool() []api.Tool { + return []api.Tool{{ + Type: "function", + Function: api.ToolFunction{ + Name: "calc", + Description: "Calculate", + Parameters: api.ToolFunctionParameters{ + Type: "object", + Properties: testPropsMap(map[string]api.ToolProperty{ + "value": {Type: api.PropertyType{"number"}, Description: "Value"}, + }), + }, + }, + }} +} + +func rawTool() []api.Tool { + return []api.Tool{{ + Type: "function", + Function: api.ToolFunction{ + Name: "raw", + Description: "Raw input", + Parameters: api.ToolFunctionParameters{ + Type: "object", + }, + }, + }} +} + +func moveTool() []api.Tool { + return []api.Tool{{ + Type: "function", + Function: api.ToolFunction{ + Name: "move", + Description: "Move", + Parameters: api.ToolFunctionParameters{ + Type: "object", + Required: []string{"x", "y"}, + Properties: testPropsMap(map[string]api.ToolProperty{ + "x": {Type: api.PropertyType{"number"}, Description: "X"}, + "y": {Type: api.PropertyType{"number"}, Description: "Y"}, + }), + }, + }, + }} +} + +func arrayNoItemsTool() []api.Tool { + return []api.Tool{{ + Type: "function", + Function: api.ToolFunction{ + Name: "tag", + Description: "Tag items", + Parameters: api.ToolFunctionParameters{ + Type: "object", + Properties: testPropsMap(map[string]api.ToolProperty{ + "tags": {Type: api.PropertyType{"array"}, Description: "Tags"}, + }), + }, + }, + }} +} + +func objectNoDescTool() []api.Tool { + return []api.Tool{{ + Type: "function", + Function: api.ToolFunction{ + Name: "update", + Description: "Update settings", + Parameters: api.ToolFunctionParameters{ + Type: "object", + Properties: testPropsMap(map[string]api.ToolProperty{ + "settings": {Type: api.PropertyType{"object"}, Properties: testPropsMap(map[string]api.ToolProperty{ + "verbose": {Type: api.PropertyType{"boolean"}, Description: "Verbose mode"}, + })}, + }), + }, + }, + }} +} + +func searchTool() []api.Tool { + return []api.Tool{{ + Type: "function", + Function: api.ToolFunction{ + Name: "search", + Description: "Search", + Parameters: api.ToolFunctionParameters{ + Type: "object", + Properties: testPropsMap(map[string]api.ToolProperty{ + "query": {Type: api.PropertyType{"string"}, Description: "Search query"}, + "limit": {Type: api.PropertyType{"number"}}, + "offset": {Type: api.PropertyType{"number"}, Description: "Start offset"}, + }), + }, + }, + }} +} + +var ( + bashSmallDeclRef = `<|tool>declaration:bash{description:<|"|>Run<|"|>,parameters:{properties:{command:{description:<|"|>Cmd<|"|>,type:<|"|>STRING<|"|>}},required:[<|"|>command<|"|>],type:<|"|>OBJECT<|"|>}}` + nestedDeclRef = `<|tool>declaration:create{description:<|"|>Create item<|"|>,parameters:{properties:{config:{description:<|"|>Config<|"|>,properties:{enabled:{description:<|"|>On/off<|"|>,type:<|"|>BOOLEAN<|"|>}},type:<|"|>OBJECT<|"|>},name:{description:<|"|>Name<|"|>,type:<|"|>STRING<|"|>}},type:<|"|>OBJECT<|"|>}}` + arrayDeclRef = `<|tool>declaration:batch{description:<|"|>Run batch<|"|>,parameters:{properties:{commands:{description:<|"|>Commands<|"|>,items:{type:<|"|>STRING<|"|>},type:<|"|>ARRAY<|"|>}},type:<|"|>OBJECT<|"|>}}` + bashDeclRef = `<|tool>declaration:bash{description:<|"|>Run a command<|"|>,parameters:{properties:{command:{description:<|"|>The command<|"|>,type:<|"|>STRING<|"|>}},required:[<|"|>command<|"|>],type:<|"|>OBJECT<|"|>}}` + readDeclRef = `<|tool>declaration:read{description:<|"|>Read a file<|"|>,parameters:{properties:{path:{description:<|"|>File path<|"|>,type:<|"|>STRING<|"|>}},required:[<|"|>path<|"|>],type:<|"|>OBJECT<|"|>}}` + weatherDeclRef = `<|tool>declaration:get_weather{description:<|"|>Get weather<|"|>,parameters:{properties:{city:{description:<|"|>City<|"|>,type:<|"|>STRING<|"|>}},type:<|"|>OBJECT<|"|>}}` + addDeclRef = `<|tool>declaration:add{description:<|"|>Add numbers<|"|>,parameters:{properties:{a:{type:<|"|>NUMBER<|"|>},b:{type:<|"|>NUMBER<|"|>}},type:<|"|>OBJECT<|"|>}}` + flagDeclRef = `<|tool>declaration:set_flag{description:<|"|>Set a flag<|"|>,parameters:{properties:{enabled:{description:<|"|>Flag value<|"|>,type:<|"|>BOOLEAN<|"|>}},type:<|"|>OBJECT<|"|>}}` + modeDeclRef = `<|tool>declaration:set_mode{description:<|"|>Set mode<|"|>,parameters:{properties:{mode:{description:<|"|>The mode<|"|>,enum:[<|"|>fast<|"|>,<|"|>slow<|"|>],type:<|"|>STRING<|"|>}},type:<|"|>OBJECT<|"|>}}` + configureDeclRef = `<|tool>declaration:configure{description:<|"|>Configure<|"|>,parameters:{properties:{config:{description:<|"|>Config<|"|>,properties:{},type:<|"|>OBJECT<|"|>}},type:<|"|>OBJECT<|"|>}}` + batchArrayDeclRef = `<|tool>declaration:batch{description:<|"|>Run batch<|"|>,parameters:{properties:{ids:{description:<|"|>IDs<|"|>,type:<|"|>ARRAY<|"|>}},type:<|"|>OBJECT<|"|>}}` + countDeclRef = `<|tool>declaration:count{description:<|"|>Count items<|"|>,parameters:{properties:{n:{type:<|"|>NUMBER<|"|>}},type:<|"|>OBJECT<|"|>}}` + enumNoDescDeclRef = `<|tool>declaration:set_level{description:<|"|>Set level<|"|>,parameters:{properties:{level:{enum:[<|"|>low<|"|>,<|"|>high<|"|>],type:<|"|>STRING<|"|>}},type:<|"|>OBJECT<|"|>}}` + searchDeclRef = `<|tool>declaration:search{description:<|"|>Search<|"|>,parameters:{properties:{limit:{type:<|"|>NUMBER<|"|>},offset:{description:<|"|>Start offset<|"|>,type:<|"|>NUMBER<|"|>},query:{description:<|"|>Search query<|"|>,type:<|"|>STRING<|"|>}},type:<|"|>OBJECT<|"|>}}` + arrayNoItemsDeclRef = `<|tool>declaration:tag{description:<|"|>Tag items<|"|>,parameters:{properties:{tags:{description:<|"|>Tags<|"|>,type:<|"|>ARRAY<|"|>}},type:<|"|>OBJECT<|"|>}}` + objectNoDescDeclRef = `<|tool>declaration:update{description:<|"|>Update settings<|"|>,parameters:{properties:{settings:{,properties:{verbose:{description:<|"|>Verbose mode<|"|>,type:<|"|>BOOLEAN<|"|>}}type:<|"|>OBJECT<|"|>}},type:<|"|>OBJECT<|"|>}}` + nestedRequiredDeclRef = `<|tool>declaration:create_user{description:<|"|>Create user<|"|>,parameters:{properties:{profile:{description:<|"|>Profile<|"|>,properties:{age:{description:<|"|>Age<|"|>,type:<|"|>NUMBER<|"|>},name:{description:<|"|>Name<|"|>,type:<|"|>STRING<|"|>}},required:[<|"|>name<|"|>],type:<|"|>OBJECT<|"|>}},type:<|"|>OBJECT<|"|>}}` + calcDeclRef = `<|tool>declaration:calc{description:<|"|>Calculate<|"|>,parameters:{properties:{value:{description:<|"|>Value<|"|>,type:<|"|>NUMBER<|"|>}},type:<|"|>OBJECT<|"|>}}` + rawDeclRef = `<|tool>declaration:raw{description:<|"|>Raw input<|"|>,parameters:{type:<|"|>OBJECT<|"|>}}` + moveDeclRef = `<|tool>declaration:move{description:<|"|>Move<|"|>,parameters:{properties:{x:{description:<|"|>X<|"|>,type:<|"|>NUMBER<|"|>},y:{description:<|"|>Y<|"|>,type:<|"|>NUMBER<|"|>}},required:[<|"|>x<|"|>,<|"|>y<|"|>],type:<|"|>OBJECT<|"|>}}` +) + +func TestGemma4RendererMatchesReference(t *testing.T) { + q := `<|"|>` + + tests := []struct { + name string + messages []api.Message + tools []api.Tool + think *api.ThinkValue + expected string + }{ + // === Header block paths === + { + name: "user_only", + messages: []api.Message{{Role: "user", Content: "Hello"}}, + expected: "<|turn>user\nHello\n<|turn>model\n", + }, + { + name: "system_user", + messages: []api.Message{ + {Role: "system", Content: "You are helpful."}, + {Role: "user", Content: "Hi"}, + }, + expected: "<|turn>system\nYou are helpful.\n<|turn>user\nHi\n<|turn>model\n", + }, + { + name: "developer_user", + messages: []api.Message{ + {Role: "developer", Content: "You are helpful."}, + {Role: "user", Content: "Hi"}, + }, + expected: "<|turn>system\nYou are helpful.\n<|turn>user\nHi\n<|turn>model\n", + }, + { + name: "tools_no_system", + messages: []api.Message{{Role: "user", Content: "Hi"}}, + tools: bashRefTool(), + expected: "<|turn>system\n" + bashDeclRef + "\n<|turn>user\nHi\n<|turn>model\n", + }, + { + name: "system_tools", + messages: []api.Message{ + {Role: "system", Content: "You are helpful."}, + {Role: "user", Content: "Hi"}, + }, + tools: bashRefTool(), + expected: "<|turn>system\nYou are helpful." + bashDeclRef + "\n<|turn>user\nHi\n<|turn>model\n", + }, + { + name: "thinking_no_system", + messages: []api.Message{{Role: "user", Content: "Hi"}}, + think: thinkTrue(), + expected: "<|turn>system\n<|think|>\n<|turn>user\nHi\n<|turn>model\n", + }, + { + name: "thinking_system", + messages: []api.Message{ + {Role: "system", Content: "You are helpful."}, + {Role: "user", Content: "Hi"}, + }, + think: thinkTrue(), + expected: "<|turn>system\n<|think|>You are helpful.\n<|turn>user\nHi\n<|turn>model\n", + }, + { + name: "thinking_tools", + messages: []api.Message{{Role: "user", Content: "Hi"}}, + tools: bashRefTool(), + think: thinkTrue(), + expected: "<|turn>system\n<|think|>" + bashDeclRef + "\n<|turn>user\nHi\n<|turn>model\n", + }, + { + name: "thinking_system_tools", + messages: []api.Message{ + {Role: "system", Content: "You are helpful."}, + {Role: "user", Content: "Hi"}, + }, + tools: bashRefTool(), + think: thinkTrue(), + expected: "<|turn>system\n<|think|>You are helpful." + bashDeclRef + "\n<|turn>user\nHi\n<|turn>model\n", + }, + + // === Message loop paths === + { + name: "multi_turn", + messages: []api.Message{ + {Role: "system", Content: "You are helpful."}, + {Role: "user", Content: "Hi"}, + {Role: "assistant", Content: "Hello!"}, + {Role: "user", Content: "More"}, + }, + expected: "<|turn>system\nYou are helpful.\n" + + "<|turn>user\nHi\n" + + "<|turn>model\nHello!\n" + + "<|turn>user\nMore\n" + + "<|turn>model\n", + }, + { + // Tool call with structured args → tool response as separate <|turn>tool turn + name: "tool_call_response", + messages: []api.Message{ + {Role: "system", Content: "You are helpful."}, + {Role: "user", Content: "List files"}, + {Role: "assistant", ToolCalls: []api.ToolCall{{ + Function: api.ToolCallFunction{ + Name: "bash", + Arguments: testArgs(map[string]any{"command": "ls"}), + }, + }}}, + {Role: "tool", Content: "file1.txt\nfile2.txt"}, + }, + tools: bashRefTool(), + expected: "<|turn>system\nYou are helpful." + bashDeclRef + "\n" + + "<|turn>user\nList files\n" + + "<|turn>model\n<|tool_call>call:bash{command:" + q + "ls" + q + "}\n" + + "<|turn>tool\nfile1.txt\nfile2.txt\n" + + "<|turn>model\n", + }, + { + // Full round trip: call → response → assistant reply → user follow-up + name: "full_round_trip", + messages: []api.Message{ + {Role: "system", Content: "You are helpful."}, + {Role: "user", Content: "List files"}, + {Role: "assistant", ToolCalls: []api.ToolCall{{ + Function: api.ToolCallFunction{ + Name: "bash", + Arguments: testArgs(map[string]any{"command": "ls"}), + }, + }}}, + {Role: "tool", Content: "file1.txt\nfile2.txt"}, + {Role: "assistant", Content: "Here are the files."}, + {Role: "user", Content: "Read file1.txt"}, + }, + tools: bashAndReadRefTools(), + expected: "<|turn>system\nYou are helpful." + bashDeclRef + readDeclRef + "\n" + + "<|turn>user\nList files\n" + + "<|turn>model\n<|tool_call>call:bash{command:" + q + "ls" + q + "}\n" + + "<|turn>tool\nfile1.txt\nfile2.txt\n" + + "<|turn>model\nHere are the files.\n" + + "<|turn>user\nRead file1.txt\n" + + "<|turn>model\n", + }, + { + // Multiple tool calls + multiple tool responses + name: "multiple_tool_calls", + messages: []api.Message{ + {Role: "system", Content: "You are helpful."}, + {Role: "user", Content: "List and read"}, + {Role: "assistant", ToolCalls: []api.ToolCall{ + {Function: api.ToolCallFunction{Name: "bash", Arguments: testArgs(map[string]any{"command": "ls"})}}, + {Function: api.ToolCallFunction{Name: "read", Arguments: testArgs(map[string]any{"path": "go.mod"})}}, + }}, + {Role: "tool", Content: "file1.txt\nfile2.txt"}, + {Role: "tool", Content: "module example.com/foo"}, + }, + tools: bashAndReadRefTools(), + expected: "<|turn>system\nYou are helpful." + bashDeclRef + readDeclRef + "\n" + + "<|turn>user\nList and read\n" + + "<|turn>model\n<|tool_call>call:bash{command:" + q + "ls" + q + "}" + + "<|tool_call>call:read{path:" + q + "go.mod" + q + "}\n" + + "<|turn>tool\nfile1.txt\nfile2.txt\n" + + "<|turn>tool\nmodule example.com/foo\n" + + "<|turn>model\n", + }, + { + // Thinking content in assistant history should be stripped + name: "strip_thinking_history", + messages: []api.Message{ + {Role: "user", Content: "What is 2+2?"}, + {Role: "assistant", Content: "<|channel>thought\nThinking...4"}, + {Role: "user", Content: "And 3+3?"}, + }, + expected: "<|turn>user\nWhat is 2+2?\n" + + "<|turn>model\n4\n" + + "<|turn>user\nAnd 3+3?\n" + + "<|turn>model\n", + }, + // === Additional edge cases ported from original tests === + { + // Assistant content with tool call — template emits tool_calls before content + name: "assistant_content_with_tool_call", + messages: []api.Message{ + {Role: "user", Content: "Weather?"}, + {Role: "assistant", Content: "Let me check.", ToolCalls: []api.ToolCall{{ + Function: api.ToolCallFunction{Name: "get_weather", Arguments: testArgs(map[string]any{"city": "Paris"})}, + }}}, + {Role: "tool", Content: "Sunny"}, + }, + tools: weatherTool(), + expected: "<|turn>system\n" + weatherDeclRef + "\n" + + "<|turn>user\nWeather?\n" + + "<|turn>model\n<|tool_call>call:get_weather{city:" + q + "Paris" + q + "}Let me check.\n" + + "<|turn>tool\nSunny\n" + + "<|turn>model\n", + }, + { + // Numeric tool call arguments + name: "numeric_arguments", + messages: []api.Message{ + {Role: "user", Content: "Add"}, + {Role: "assistant", ToolCalls: []api.ToolCall{{ + Function: api.ToolCallFunction{Name: "add", Arguments: testArgs(map[string]any{"a": float64(1), "b": float64(2)})}, + }}}, + {Role: "tool", Content: `{"result": 3}`}, + }, + tools: addTool(), + expected: "<|turn>system\n" + addDeclRef + "\n" + + "<|turn>user\nAdd\n" + + "<|turn>model\n<|tool_call>call:add{a:1,b:2}\n" + + "<|turn>tool\n{\"result\": 3}\n" + + "<|turn>model\n", + }, + { + // Boolean tool call argument + name: "boolean_argument", + messages: []api.Message{ + {Role: "user", Content: "Set flag"}, + {Role: "assistant", ToolCalls: []api.ToolCall{{ + Function: api.ToolCallFunction{Name: "set_flag", Arguments: testArgs(map[string]any{"enabled": true})}, + }}}, + {Role: "tool", Content: "done"}, + }, + tools: flagTool(), + expected: "<|turn>system\n" + flagDeclRef + "\n" + + "<|turn>user\nSet flag\n" + + "<|turn>model\n<|tool_call>call:set_flag{enabled:true}\n" + + "<|turn>tool\ndone\n" + + "<|turn>model\n", + }, + { + // Tool with enum parameter + name: "tool_with_enum", + messages: []api.Message{{Role: "user", Content: "Test"}}, + tools: modeTool(), + expected: "<|turn>system\n" + modeDeclRef + "\n" + + "<|turn>user\nTest\n<|turn>model\n", + }, + { + name: "unicode_content", + messages: []api.Message{{Role: "user", Content: "こんにちは"}}, + expected: "<|turn>user\nこんにちは\n<|turn>model\n", + }, + { + name: "newlines_in_content", + messages: []api.Message{{Role: "user", Content: "Line 1\nLine 2\nLine 3"}}, + expected: "<|turn>user\nLine 1\nLine 2\nLine 3\n<|turn>model\n", + }, + { + // Tool response (raw JSON) followed by user message + name: "json_tool_response_then_user", + messages: []api.Message{ + {Role: "user", Content: "Weather?"}, + {Role: "assistant", ToolCalls: []api.ToolCall{{ + Function: api.ToolCallFunction{Name: "get_weather", Arguments: testArgs(map[string]any{"city": "Tokyo"})}, + }}}, + {Role: "tool", Content: `{"temperature": 15, "weather": "sunny"}`}, + {Role: "user", Content: "Thanks!"}, + }, + tools: weatherTool(), + expected: "<|turn>system\n" + weatherDeclRef + "\n" + + "<|turn>user\nWeather?\n" + + "<|turn>model\n<|tool_call>call:get_weather{city:" + q + "Tokyo" + q + "}\n" + + "<|turn>tool\n{\"temperature\": 15, \"weather\": \"sunny\"}\n" + + "<|turn>user\nThanks!\n" + + "<|turn>model\n", + }, + // === Ordering and whitespace edge cases === + { + // Tool call arguments are sorted alphabetically + name: "sorted_args", + messages: []api.Message{ + {Role: "user", Content: "Go"}, + {Role: "assistant", ToolCalls: []api.ToolCall{{ + Function: api.ToolCallFunction{Name: "bash", Arguments: testArgs(map[string]any{"zzz": "last", "aaa": "first", "mmm": "middle"})}, + }}}, + {Role: "tool", Content: "ok"}, + }, + tools: bashSmallTool(), + expected: "<|turn>system\n" + bashSmallDeclRef + "\n" + + "<|turn>user\nGo\n" + + "<|turn>model\n<|tool_call>call:bash{aaa:" + q + "first" + q + ",mmm:" + q + "middle" + q + ",zzz:" + q + "last" + q + "}\n" + + "<|turn>tool\nok\n" + + "<|turn>model\n", + }, + { + // User content with whitespace is trimmed + name: "user_content_trimmed", + messages: []api.Message{{Role: "user", Content: " hello "}}, + expected: "<|turn>user\nhello\n<|turn>model\n", + }, + { + // Empty tool call arguments + name: "empty_tool_args", + messages: []api.Message{ + {Role: "user", Content: "Go"}, + {Role: "assistant", ToolCalls: []api.ToolCall{{ + Function: api.ToolCallFunction{Name: "bash", Arguments: testArgs(map[string]any{})}, + }}}, + {Role: "tool", Content: "ok"}, + }, + tools: bashSmallTool(), + expected: "<|turn>system\n" + bashSmallDeclRef + "\n" + + "<|turn>user\nGo\n" + + "<|turn>model\n<|tool_call>call:bash{}\n" + + "<|turn>tool\nok\n" + + "<|turn>model\n", + }, + { + // Nested object properties in tool declaration + name: "nested_object_tool", + messages: []api.Message{{Role: "user", Content: "Create"}}, + tools: nestedTool(), + expected: "<|turn>system\n" + nestedDeclRef + "\n" + + "<|turn>user\nCreate\n<|turn>model\n", + }, + { + // Array type in tool declaration + name: "array_tool", + messages: []api.Message{{Role: "user", Content: "Batch"}}, + tools: arrayTool(), + expected: "<|turn>system\n" + arrayDeclRef + "\n" + + "<|turn>user\nBatch\n<|turn>model\n", + }, + { + // Assistant whitespace is trimmed (strip_thinking includes | trim) + name: "assistant_whitespace_trimmed", + messages: []api.Message{ + {Role: "user", Content: "Hi"}, + {Role: "assistant", Content: " spaced "}, + {Role: "user", Content: "More"}, + }, + expected: "<|turn>user\nHi\n" + + "<|turn>model\nspaced\n" + + "<|turn>user\nMore\n" + + "<|turn>model\n", + }, + { + // Three sequential tool responses + name: "three_tool_responses", + messages: []api.Message{ + {Role: "user", Content: "Do three things"}, + {Role: "assistant", ToolCalls: []api.ToolCall{ + {Function: api.ToolCallFunction{Name: "bash", Arguments: testArgs(map[string]any{"command": "a"})}}, + {Function: api.ToolCallFunction{Name: "bash", Arguments: testArgs(map[string]any{"command": "b"})}}, + {Function: api.ToolCallFunction{Name: "bash", Arguments: testArgs(map[string]any{"command": "c"})}}, + }}, + {Role: "tool", Content: "result-a"}, + {Role: "tool", Content: "result-b"}, + {Role: "tool", Content: "result-c"}, + }, + tools: bashSmallTool(), + expected: "<|turn>system\n" + bashSmallDeclRef + "\n" + + "<|turn>user\nDo three things\n" + + "<|turn>model\n<|tool_call>call:bash{command:" + q + "a" + q + "}" + + "<|tool_call>call:bash{command:" + q + "b" + q + "}" + + "<|tool_call>call:bash{command:" + q + "c" + q + "}\n" + + "<|turn>tool\nresult-a\n" + + "<|turn>tool\nresult-b\n" + + "<|turn>tool\nresult-c\n" + + "<|turn>model\n", + }, + { + // Assistant with only tool calls, no content field + name: "tool_calls_no_content", + messages: []api.Message{ + {Role: "user", Content: "Go"}, + {Role: "assistant", ToolCalls: []api.ToolCall{{ + Function: api.ToolCallFunction{Name: "bash", Arguments: testArgs(map[string]any{"command": "ls"})}, + }}}, + {Role: "tool", Content: "files"}, + }, + tools: bashSmallTool(), + expected: "<|turn>system\n" + bashSmallDeclRef + "\n" + + "<|turn>user\nGo\n" + + "<|turn>model\n<|tool_call>call:bash{command:" + q + "ls" + q + "}\n" + + "<|turn>tool\nfiles\n" + + "<|turn>model\n", + }, + + // === Coverage gap cases === + { + // Multiple thinking blocks stripped from assistant history + name: "multiple_thinking_blocks", + messages: []api.Message{ + {Role: "user", Content: "Hi"}, + {Role: "assistant", Content: "<|channel>Think1Middle<|channel>Think2Done"}, + {Role: "user", Content: "More"}, + }, + expected: "<|turn>user\nHi\n" + + "<|turn>model\nMiddleDone\n" + + "<|turn>user\nMore\n" + + "<|turn>model\n", + }, + { + // Property with no description — just type + name: "property_no_description", + messages: []api.Message{{Role: "user", Content: "Count"}}, + tools: countTool(), + expected: "<|turn>system\n" + countDeclRef + "\n" + + "<|turn>user\nCount\n<|turn>model\n", + }, + { + // System message with leading/trailing whitespace is trimmed + name: "system_message_trimmed", + messages: []api.Message{ + {Role: "system", Content: " You are helpful. "}, + {Role: "user", Content: "Hi"}, + }, + expected: "<|turn>system\nYou are helpful.\n" + + "<|turn>user\nHi\n<|turn>model\n", + }, + { + // Deeply nested map in tool call arguments (3 levels) + name: "nested_map_args", + messages: []api.Message{ + {Role: "user", Content: "Go"}, + {Role: "assistant", ToolCalls: []api.ToolCall{{ + Function: api.ToolCallFunction{Name: "configure", Arguments: testArgs(map[string]any{ + "config": map[string]any{"db": map[string]any{"host": "localhost", "port": float64(5432)}}, + })}, + }}}, + {Role: "tool", Content: "ok"}, + }, + tools: configureTool(), + expected: "<|turn>system\n" + configureDeclRef + "\n" + + "<|turn>user\nGo\n" + + "<|turn>model\n<|tool_call>call:configure{config:{db:{host:" + q + "localhost" + q + ",port:5432}}}\n" + + "<|turn>tool\nok\n" + + "<|turn>model\n", + }, + { + // Array values in tool call arguments + name: "array_in_args", + messages: []api.Message{ + {Role: "user", Content: "Go"}, + {Role: "assistant", ToolCalls: []api.ToolCall{{ + Function: api.ToolCallFunction{Name: "batch", Arguments: testArgs(map[string]any{ + "ids": []any{float64(1), float64(2), float64(3)}, + })}, + }}}, + {Role: "tool", Content: "done"}, + }, + tools: batchArrayTool(), + expected: "<|turn>system\n" + batchArrayDeclRef + "\n" + + "<|turn>user\nGo\n" + + "<|turn>model\n<|tool_call>call:batch{ids:[1,2,3]}\n" + + "<|turn>tool\ndone\n" + + "<|turn>model\n", + }, + { + // Mixed types in array argument (string, number, bool) + name: "mixed_array_args", + messages: []api.Message{ + {Role: "user", Content: "Go"}, + {Role: "assistant", ToolCalls: []api.ToolCall{{ + Function: api.ToolCallFunction{Name: "batch", Arguments: testArgs(map[string]any{ + "ids": []any{"a", float64(1), true}, + })}, + }}}, + {Role: "tool", Content: "done"}, + }, + tools: batchArrayTool(), + expected: "<|turn>system\n" + batchArrayDeclRef + "\n" + + "<|turn>user\nGo\n" + + "<|turn>model\n<|tool_call>call:batch{ids:[" + q + "a" + q + ",1,true]}\n" + + "<|turn>tool\ndone\n" + + "<|turn>model\n", + }, + { + // Enum property without description + name: "enum_no_description", + messages: []api.Message{{Role: "user", Content: "Set"}}, + tools: enumNoDescTool(), + expected: "<|turn>system\n" + enumNoDescDeclRef + "\n" + + "<|turn>user\nSet\n<|turn>model\n", + }, + { + // System message that is only whitespace (trims to empty) + name: "system_whitespace_only", + messages: []api.Message{ + {Role: "system", Content: " "}, + {Role: "user", Content: "Hi"}, + }, + expected: "<|turn>system\n\n" + + "<|turn>user\nHi\n<|turn>model\n", + }, + { + // Empty assistant content (empty string, not nil) + name: "empty_assistant_content", + messages: []api.Message{ + {Role: "user", Content: "Hi"}, + {Role: "assistant", Content: ""}, + {Role: "user", Content: "More"}, + }, + expected: "<|turn>user\nHi\n" + + "<|turn>model\n\n" + + "<|turn>user\nMore\n" + + "<|turn>model\n", + }, + { + // Map argument with string keys (keys NOT escaped with <|"|>) + name: "map_arg_string_keys", + messages: []api.Message{ + {Role: "user", Content: "Go"}, + {Role: "assistant", ToolCalls: []api.ToolCall{{ + Function: api.ToolCallFunction{Name: "configure", Arguments: testArgs(map[string]any{ + "config": map[string]any{"key": "value"}, + })}, + }}}, + {Role: "tool", Content: "ok"}, + }, + tools: configureTool(), + expected: "<|turn>system\n" + configureDeclRef + "\n" + + "<|turn>user\nGo\n" + + "<|turn>model\n<|tool_call>call:configure{config:{key:" + q + "value" + q + "}}\n" + + "<|turn>tool\nok\n" + + "<|turn>model\n", + }, + { + // Mixed properties: some with description, some without + name: "mixed_desc_no_desc", + messages: []api.Message{{Role: "user", Content: "Search"}}, + tools: searchTool(), + expected: "<|turn>system\n" + searchDeclRef + "\n" + + "<|turn>user\nSearch\n<|turn>model\n", + }, + + // === Round 3 coverage gaps === + { + // Tool content with whitespace is trimmed (template does | trim for all non-model) + name: "tool_content_trimmed", + messages: []api.Message{ + {Role: "user", Content: "Go"}, + {Role: "assistant", ToolCalls: []api.ToolCall{{ + Function: api.ToolCallFunction{Name: "bash", Arguments: testArgs(map[string]any{"command": "ls"})}, + }}}, + {Role: "tool", Content: " result "}, + }, + tools: bashSmallTool(), + expected: "<|turn>system\n" + bashSmallDeclRef + "\n" + + "<|turn>user\nGo\n" + + "<|turn>model\n<|tool_call>call:bash{command:" + q + "ls" + q + "}\n" + + "<|turn>tool\nresult\n" + + "<|turn>model\n", + }, + { + // Empty system message still emits system turn + name: "empty_system_message", + messages: []api.Message{ + {Role: "system", Content: ""}, + {Role: "user", Content: "Hi"}, + }, + expected: "<|turn>system\n\n" + + "<|turn>user\nHi\n<|turn>model\n", + }, + { + // Nested OBJECT property with required field + name: "nested_object_with_required", + messages: []api.Message{{Role: "user", Content: "Create"}}, + tools: nestedRequiredTool(), + expected: "<|turn>system\n" + nestedRequiredDeclRef + "\n" + + "<|turn>user\nCreate\n<|turn>model\n", + }, + { + // Non-integer float in tool call argument + name: "float_argument", + messages: []api.Message{ + {Role: "user", Content: "Calc"}, + {Role: "assistant", ToolCalls: []api.ToolCall{{ + Function: api.ToolCallFunction{Name: "calc", Arguments: testArgs(map[string]any{"value": 3.14})}, + }}}, + {Role: "tool", Content: "ok"}, + }, + tools: calcTool(), + expected: "<|turn>system\n" + calcDeclRef + "\n" + + "<|turn>user\nCalc\n" + + "<|turn>model\n<|tool_call>call:calc{value:3.14}\n" + + "<|turn>tool\nok\n" + + "<|turn>model\n", + }, + { + // Thinking in the last assistant message (stripped before generation prompt) + name: "thinking_in_last_assistant", + messages: []api.Message{ + {Role: "user", Content: "Hi"}, + {Role: "assistant", Content: "<|channel>thinkingResult"}, + }, + expected: "<|turn>user\nHi\n" + + "<|turn>model\nResult\n" + + "<|turn>model\n", + }, + { + // Tool content with newlines and leading/trailing whitespace trimmed + name: "tool_content_multiline_whitespace", + messages: []api.Message{ + {Role: "user", Content: "Go"}, + {Role: "assistant", ToolCalls: []api.ToolCall{{ + Function: api.ToolCallFunction{Name: "bash", Arguments: testArgs(map[string]any{"command": "ls"})}, + }}}, + {Role: "tool", Content: "\n file1\n file2\n"}, + }, + tools: bashSmallTool(), + expected: "<|turn>system\n" + bashSmallDeclRef + "\n" + + "<|turn>user\nGo\n" + + "<|turn>model\n<|tool_call>call:bash{command:" + q + "ls" + q + "}\n" + + "<|turn>tool\nfile1\n file2\n" + + "<|turn>model\n", + }, + { + // Tool with parameters having only type, no properties + name: "tool_params_type_only", + messages: []api.Message{{Role: "user", Content: "Raw"}}, + tools: rawTool(), + expected: "<|turn>system\n" + rawDeclRef + "\n" + + "<|turn>user\nRaw\n<|turn>model\n", + }, + { + // Multiple required fields at top level + name: "multiple_required", + messages: []api.Message{{Role: "user", Content: "Move"}}, + tools: moveTool(), + expected: "<|turn>system\n" + moveDeclRef + "\n" + + "<|turn>user\nMove\n<|turn>model\n", + }, + { + // Assistant content that is ONLY thinking (strips to empty) + name: "assistant_only_thinking", + messages: []api.Message{ + {Role: "user", Content: "Hi"}, + {Role: "assistant", Content: "<|channel>just thinking"}, + {Role: "user", Content: "More"}, + }, + expected: "<|turn>user\nHi\n" + + "<|turn>model\n\n" + + "<|turn>user\nMore\n" + + "<|turn>model\n", + }, + + // === Round 4: final coverage gaps === + { + // Thinking enabled with tool calls in same conversation (full agentic scenario) + name: "thinking_with_tool_calls", + messages: []api.Message{ + {Role: "system", Content: "You are helpful."}, + {Role: "user", Content: "List files"}, + {Role: "assistant", Content: "<|channel>I should use bash", ToolCalls: []api.ToolCall{{ + Function: api.ToolCallFunction{Name: "bash", Arguments: testArgs(map[string]any{"command": "ls"})}, + }}}, + {Role: "tool", Content: "file1.txt"}, + {Role: "assistant", Content: "Here are the files."}, + {Role: "user", Content: "Thanks"}, + }, + tools: bashSmallTool(), + think: thinkTrue(), + expected: "<|turn>system\n<|think|>You are helpful." + bashSmallDeclRef + "\n" + + "<|turn>user\nList files\n" + + "<|turn>model\n<|tool_call>call:bash{command:" + q + "ls" + q + "}\n" + + "<|turn>tool\nfile1.txt\n" + + "<|turn>model\nHere are the files.\n" + + "<|turn>user\nThanks\n" + + "<|turn>model\n", + }, + { + // Array property without items specification + name: "array_without_items", + messages: []api.Message{{Role: "user", Content: "Tag"}}, + tools: arrayNoItemsTool(), + expected: "<|turn>system\n" + arrayNoItemsDeclRef + "\n" + + "<|turn>user\nTag\n<|turn>model\n", + }, + { + // OBJECT property without description but with nested properties — + // template hardcodes leading comma on ,properties: and does NOT + // add comma before type: when description is absent + name: "object_no_desc_with_properties", + messages: []api.Message{{Role: "user", Content: "Update"}}, + tools: objectNoDescTool(), + expected: "<|turn>system\n" + objectNoDescDeclRef + "\n" + + "<|turn>user\nUpdate\n<|turn>model\n", + }, + + // === Round 5: coding agent patterns === + { + // Chained tool calls — assistant calls tool, gets result, calls another + // tool, gets result, then the model responds. No user messages in between. + name: "chained_tool_calls", + messages: []api.Message{ + {Role: "user", Content: "Set up the project"}, + {Role: "assistant", ToolCalls: []api.ToolCall{{ + Function: api.ToolCallFunction{Name: "bash", Arguments: testArgs(map[string]any{"command": "mkdir src"})}, + }}}, + {Role: "tool", Content: ""}, + {Role: "assistant", ToolCalls: []api.ToolCall{{ + Function: api.ToolCallFunction{Name: "bash", Arguments: testArgs(map[string]any{"command": "touch src/main.go"})}, + }}}, + {Role: "tool", Content: ""}, + {Role: "assistant", Content: "Done."}, + {Role: "user", Content: "Thanks"}, + }, + tools: bashSmallTool(), + expected: "<|turn>system\n" + bashSmallDeclRef + "\n" + + "<|turn>user\nSet up the project\n" + + "<|turn>model\n<|tool_call>call:bash{command:" + q + "mkdir src" + q + "}\n" + + "<|turn>tool\n\n" + + "<|turn>model\n<|tool_call>call:bash{command:" + q + "touch src/main.go" + q + "}\n" + + "<|turn>tool\n\n" + + "<|turn>model\nDone.\n" + + "<|turn>user\nThanks\n" + + "<|turn>model\n", + }, + { + // Tool call with thinking that strips to real remaining content + name: "tool_call_thinking_with_remaining_content", + messages: []api.Message{ + {Role: "user", Content: "List files"}, + {Role: "assistant", Content: "<|channel>I need to check the directoryLet me list the files.", ToolCalls: []api.ToolCall{{ + Function: api.ToolCallFunction{Name: "bash", Arguments: testArgs(map[string]any{"command": "ls"})}, + }}}, + {Role: "tool", Content: "main.go\ngo.mod"}, + {Role: "user", Content: "OK"}, + }, + tools: bashSmallTool(), + expected: "<|turn>system\n" + bashSmallDeclRef + "\n" + + "<|turn>user\nList files\n" + + "<|turn>model\n<|tool_call>call:bash{command:" + q + "ls" + q + "}Let me list the files.\n" + + "<|turn>tool\nmain.go\ngo.mod\n" + + "<|turn>user\nOK\n" + + "<|turn>model\n", + }, + { + // Argument value containing newlines (multi-line script) + name: "argument_with_newlines", + messages: []api.Message{ + {Role: "user", Content: "Run it"}, + {Role: "assistant", ToolCalls: []api.ToolCall{{ + Function: api.ToolCallFunction{Name: "bash", Arguments: testArgs(map[string]any{"command": "echo hello\necho world"})}, + }}}, + {Role: "tool", Content: "hello\nworld"}, + }, + tools: bashSmallTool(), + expected: "<|turn>system\n" + bashSmallDeclRef + "\n" + + "<|turn>user\nRun it\n" + + "<|turn>model\n<|tool_call>call:bash{command:" + q + "echo hello\necho world" + q + "}\n" + + "<|turn>tool\nhello\nworld\n" + + "<|turn>model\n", + }, + { + // Empty string argument value + name: "empty_string_argument", + messages: []api.Message{ + {Role: "user", Content: "Go"}, + {Role: "assistant", ToolCalls: []api.ToolCall{{ + Function: api.ToolCallFunction{Name: "bash", Arguments: testArgs(map[string]any{"command": ""})}, + }}}, + {Role: "tool", Content: "error"}, + }, + tools: bashSmallTool(), + expected: "<|turn>system\n" + bashSmallDeclRef + "\n" + + "<|turn>user\nGo\n" + + "<|turn>model\n<|tool_call>call:bash{command:" + q + q + "}\n" + + "<|turn>tool\nerror\n" + + "<|turn>model\n", + }, + } + + verifyJinja2 := os.Getenv("VERIFY_JINJA2") != "" + if verifyJinja2 { + // Verify python3 and jinja2 are available + if err := exec.Command("python3", "-c", "import jinja2").Run(); err != nil { + t.Fatal("VERIFY_JINJA2=1 requires python3 with jinja2: pip install jinja2") + } + t.Log("VERIFY_JINJA2=1: verifying expected values against Jinja2 template") + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Compare our renderer against the hardcoded expected value + renderer := &Gemma4Renderer{useImgTags: RenderImgTags} + got, err := renderer.Render(tt.messages, tt.tools, tt.think) + assert.NoError(t, err) + assert.Equal(t, tt.expected, got) + + // When VERIFY_JINJA2=1, also verify the expected value against + // the real Jinja2 template rendered by Python. + if verifyJinja2 { + jinja2Output := renderWithJinja2(t, tt.messages, tt.tools, tt.think) + if jinja2Output != tt.expected || jinja2Output != got { + fmt.Fprintf(os.Stderr, "\nJINJA2 OUTPUT for %s (copy-paste as expected):\n%q\n\n", tt.name, jinja2Output) + } + assert.Equal(t, jinja2Output, tt.expected, + "hardcoded expected value doesn't match Jinja2 template output") + assert.Equal(t, jinja2Output, got, + "renderer output doesn't match Jinja2 template output") + } + }) + } +} + +// renderWithJinja2 shells out to python3 to render messages through the +// Jinja2 chat template. Returns the rendered string. +func renderWithJinja2(t *testing.T, messages []api.Message, tools []api.Tool, think *api.ThinkValue) string { + t.Helper() + + templatePath, err := filepath.Abs("testdata/gemma4_chat_template.jinja2") + if err != nil { + t.Fatalf("failed to get template path: %v", err) + } + + // Convert messages to the format the Jinja2 template expects. + // The template uses message['tool_calls'] with function.arguments as a dict. + type jinja2ToolCall struct { + Function struct { + Name string `json:"name"` + Arguments any `json:"arguments"` + } `json:"function"` + } + type jinja2Message struct { + Role string `json:"role"` + Content string `json:"content,omitempty"` + ToolCalls []jinja2ToolCall `json:"tool_calls,omitempty"` + } + + var jMsgs []jinja2Message + for _, m := range messages { + jm := jinja2Message{Role: m.Role, Content: m.Content} + for _, tc := range m.ToolCalls { + jtc := jinja2ToolCall{} + jtc.Function.Name = tc.Function.Name + // Convert ToolCallFunctionArguments to a map + var args map[string]any + raw, _ := tc.Function.Arguments.MarshalJSON() + json.Unmarshal(raw, &args) + jtc.Function.Arguments = args + jm.ToolCalls = append(jm.ToolCalls, jtc) + } + jMsgs = append(jMsgs, jm) + } + + msgsJSON, err := json.Marshal(jMsgs) + if err != nil { + t.Fatalf("failed to marshal messages: %v", err) + } + + toolsJSON := "None" + if len(tools) > 0 { + b, _ := json.Marshal(tools) + toolsJSON = string(b) + } + + thinking := "False" + if think != nil && think.Bool() { + thinking = "True" + } + + script := fmt.Sprintf(` +import json +from jinja2 import Environment +tmpl = Environment().from_string(open(%q).read()) +msgs = json.loads(%q) +tools = json.loads(%q) if %q != "None" else None +kwargs = {"messages": msgs, "bos_token": "", "add_generation_prompt": True} +if tools: + kwargs["tools"] = tools +if %s: + kwargs["enable_thinking"] = True +print(tmpl.render(**kwargs), end="") +`, templatePath, string(msgsJSON), toolsJSON, toolsJSON, thinking) + + cmd := exec.Command("python3", "-c", script) + var stdout, stderr strings.Builder + cmd.Stdout = &stdout + cmd.Stderr = &stderr + if err := cmd.Run(); err != nil { + t.Fatalf("python3 failed: %v\nstderr: %s", err, stderr.String()) + } + return stdout.String() +} + +func thinkTrue() *api.ThinkValue { + return &api.ThinkValue{Value: true} +} diff --git a/model/renderers/renderer.go b/model/renderers/renderer.go index 17acaf0b4..f63eb36fd 100644 --- a/model/renderers/renderer.go +++ b/model/renderers/renderer.go @@ -81,6 +81,8 @@ func rendererForName(name string) Renderer { return renderer case "nemotron-3-nano": return &Nemotron3NanoRenderer{} + case "gemma4": + return &Gemma4Renderer{useImgTags: RenderImgTags} case "functiongemma": return &FunctionGemmaRenderer{} case "glm-4.7": diff --git a/model/renderers/testdata/gemma4_chat_template.jinja2 b/model/renderers/testdata/gemma4_chat_template.jinja2 new file mode 100644 index 000000000..bad629b31 --- /dev/null +++ b/model/renderers/testdata/gemma4_chat_template.jinja2 @@ -0,0 +1,263 @@ +{%- macro format_parameters(properties, required) -%} + {%- set standard_keys = ['description', 'type', 'properties', 'required', 'nullable'] -%} + {%- set ns = namespace(found_first=false) -%} + {%- for key, value in properties | dictsort -%} + {%- set add_comma = false -%} + {%- if key not in standard_keys -%} + {%- if ns.found_first %},{% endif -%} + {%- set ns.found_first = true -%} + {{ key }}:{ + {%- if value['description'] -%} + description:<|"|>{{ value['description'] }}<|"|> + {%- set add_comma = true -%} + {%- endif -%} + {%- if value['nullable'] %} + {%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%} + nullable:true + {%- endif -%} + {%- if value['type'] | upper == 'STRING' -%} + {%- if value['enum'] -%} + {%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%} + enum:{{ format_argument(value['enum']) }} + {%- endif -%} + {%- elif value['type'] | upper == 'OBJECT' -%} + ,properties:{ + {%- if value['properties'] is defined and value['properties'] is mapping -%} + {{- format_parameters(value['properties'], value['required'] | default([])) -}} + {%- elif value is mapping -%} + {{- format_parameters(value, value['required'] | default([])) -}} + {%- endif -%} + } + {%- if value['required'] -%} + ,required:[ + {%- for item in value['required'] | default([]) -%} + <|"|>{{- item -}}<|"|> + {%- if not loop.last %},{% endif -%} + {%- endfor -%} + ] + {%- endif -%} + {%- elif value['type'] | upper == 'ARRAY' -%} + {%- if value['items'] is mapping and value['items'] -%} + ,items:{ + {%- set ns_items = namespace(found_first=false) -%} + {%- for item_key, item_value in value['items'] | dictsort -%} + {%- if item_value is not none -%} + {%- if ns_items.found_first %},{% endif -%} + {%- set ns_items.found_first = true -%} + {%- if item_key == 'properties' -%} + properties:{ + {%- if item_value is mapping -%} + {{- format_parameters(item_value, value['items']['required'] | default([])) -}} + {%- endif -%} + } + {%- elif item_key == 'required' -%} + required:[ + {%- for req_item in item_value -%} + <|"|>{{- req_item -}}<|"|> + {%- if not loop.last %},{% endif -%} + {%- endfor -%} + ] + {%- elif item_key == 'type' -%} + {%- if item_value is string -%} + type:{{ format_argument(item_value | upper) }} + {%- else -%} + type:{{ format_argument(item_value | map('upper') | list) }} + {%- endif -%} + {%- else -%} + {{ item_key }}:{{ format_argument(item_value) }} + {%- endif -%} + {%- endif -%} + {%- endfor -%} + } + {%- endif -%} + {%- endif -%} + {%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%} + type:<|"|>{{ value['type'] | upper }}<|"|>} + {%- endif -%} + {%- endfor -%} +{%- endmacro -%} +{%- macro format_function_declaration(tool_data) -%} + declaration:{{- tool_data['function']['name'] -}}{description:<|"|>{{- tool_data['function']['description'] -}}<|"|> + {%- set params = tool_data['function']['parameters'] -%} + {%- if params -%} + ,parameters:{ + {%- if params['properties'] -%} + properties:{ {{- format_parameters(params['properties'], params['required']) -}} }, + {%- endif -%} + {%- if params['required'] -%} + required:[ + {%- for item in params['required'] -%} + <|"|>{{- item -}}<|"|> + {{- ',' if not loop.last -}} + {%- endfor -%} + ], + {%- endif -%} + {%- if params['type'] -%} + type:<|"|>{{- params['type'] | upper -}}<|"|>} + {%- endif -%} + {%- endif -%} + {%- if 'response' in tool_data['function'] -%} + {%- set response_declaration = tool_data['function']['response'] -%} + ,response:{ + {%- if response_declaration['description'] -%} + description:<|"|>{{- response_declaration['description'] -}}<|"|>, + {%- endif -%} + {%- if response_declaration['type'] | upper == 'OBJECT' -%} + type:<|"|>{{- response_declaration['type'] | upper -}}<|"|>} + {%- endif -%} + {%- endif -%} + } +{%- endmacro -%} +{%- macro format_argument(argument, escape_keys=True) -%} + {%- if argument is string -%} + {{- '<|"|>' + argument + '<|"|>' -}} + {%- elif argument is boolean -%} + {{- 'true' if argument else 'false' -}} + {%- elif argument is mapping -%} + {{- '{' -}} + {%- set ns = namespace(found_first=false) -%} + {%- for key, value in argument | dictsort -%} + {%- if ns.found_first %},{% endif -%} + {%- set ns.found_first = true -%} + {%- if escape_keys -%} + {{- '<|"|>' + key + '<|"|>' -}} + {%- else -%} + {{- key -}} + {%- endif -%} + :{{- format_argument(value, escape_keys=escape_keys) -}} + {%- endfor -%} + {{- '}' -}} + {%- elif argument is sequence -%} + {{- '[' -}} + {%- for item in argument -%} + {{- format_argument(item, escape_keys=escape_keys) -}} + {%- if not loop.last %},{% endif -%} + {%- endfor -%} + {{- ']' -}} + {%- else -%} + {{- argument -}} + {%- endif -%} +{%- endmacro -%} +{%- macro strip_thinking(text) -%} + {%- set ns = namespace(result='') -%} + {%- for part in text.split('') -%} + {%- if '<|channel>' in part -%} + {%- set ns.result = ns.result + part.split('<|channel>')[0] -%} + {%- else -%} + {%- set ns.result = ns.result + part -%} + {%- endif -%} + {%- endfor -%} + {{- ns.result | trim -}} +{%- endmacro -%} + +{%- set ns = namespace(prev_message_type=None) -%} +{%- set loop_messages = messages -%} +{{ bos_token }} +{#- Handle System/Tool Definitions Block -#} +{%- if (enable_thinking is defined and enable_thinking) or tools or messages[0]['role'] in ['system', 'developer'] -%} + {{- '<|turn>system\n' -}} + + {#- Inject Thinking token at the very top of the FIRST system turn -#} + {%- if enable_thinking is defined and enable_thinking -%} + {{- '<|think|>' -}} + {%- set ns.prev_message_type = 'think' -%} + {%- endif -%} + + {%- if messages[0]['role'] in ['system', 'developer'] -%} + {{- messages[0]['content'] | trim -}} + {%- set loop_messages = messages[1:] -%} + {%- endif -%} + + {%- if tools -%} + {%- for tool in tools %} + {{- '<|tool>' -}} + {{- format_function_declaration(tool) | trim -}} + {{- '' -}} + {%- endfor %} + {%- set ns.prev_message_type = 'tool' -%} + {%- endif -%} + + {{- '\n' -}} +{%- endif %} + +{#- Loop through messages -#} +{%- for message in loop_messages -%} + {%- set ns.prev_message_type = None -%} + {%- set role = 'model' if message['role'] == 'assistant' else message['role'] -%} + {{- '<|turn>' + role + '\n' }} + + {%- if message['tool_calls'] -%} + {%- for tool_call in message['tool_calls'] -%} + {%- set function = tool_call['function'] -%} + {{- '<|tool_call>call:' + function['name'] + '{' -}} + {%- if function['arguments'] is mapping -%} + {%- set ns_args = namespace(found_first=false) -%} + {%- for key, value in function['arguments'] | dictsort -%} + {%- if ns_args.found_first %},{% endif -%} + {%- set ns_args.found_first = true -%} + {{- key -}}:{{- format_argument(value, escape_keys=False) -}} + {%- endfor -%} + {%- elif function['arguments'] is string -%} + {{- function['arguments'] -}} + {%- endif -%} + {{- '}' -}} + {%- endfor -%} + {%- set ns.prev_message_type = 'tool_call' -%} + {%- endif -%} + + {%- if message['tool_responses'] -%} + {#- Tool Response handling -#} + {%- for tool_response in message['tool_responses'] -%} + {{- '<|tool_response>' -}} + {%- if tool_response['response'] is mapping -%} + {{- 'response:' + tool_response['name'] | default('unknown') + '{' -}} + {%- for key, value in tool_response['response'] | dictsort -%} + {{- key -}}:{{- format_argument(value, escape_keys=False) -}} + {%- if not loop.last %},{% endif -%} + {%- endfor -%} + {{- '}' -}} + {%- else -%} + {{- 'response:' + tool_response['name'] | default('unknown') + '{value:' + format_argument(tool_response['response'], escape_keys=False) + '}' -}} + {%- endif -%} + {{- '' -}} + {%- endfor -%} + {%- set ns.prev_message_type = 'tool_response' -%} + {%- endif -%} + + {%- if message['content'] is string -%} + {%- if role == 'model' -%} + {{- strip_thinking(message['content']) -}} + {%- else -%} + {{- message['content'] | trim -}} + {%- endif -%} + {%- elif message['content'] is sequence -%} + {%- for item in message['content'] -%} + {%- if item['type'] == 'text' -%} + {%- if role == 'model' -%} + {{- strip_thinking(item['text']) -}} + {%- else -%} + {{- item['text'] | trim -}} + {%- endif -%} + {%- elif item['type'] == 'image' -%} + {{- '\n\n<|image|>\n\n' -}} + {%- set ns.prev_message_type = 'image' -%} + {%- elif item['type'] == 'audio' -%} + {{- '<|audio|>' -}} + {%- set ns.prev_message_type = 'audio' -%} + {%- elif item['type'] == 'video' -%} + {{- '\n\n<|video|>\n\n' -}} + {%- set ns.prev_message_type = 'video' -%} + {%- endif -%} + {%- endfor -%} + {%- endif -%} + + {%- if not (message['tool_responses'] and not message['content']) -%} + {{- '\n' -}} + {%- endif -%} +{%- endfor -%} + +{%- if add_generation_prompt -%} + {%- if ns.prev_message_type != 'tool_response' -%} + {{- '<|turn>model\n' -}} + {%- endif -%} +{%- endif -%} diff --git a/openai/openai.go b/openai/openai.go index 0dfe95319..d77bcf7e1 100644 --- a/openai/openai.go +++ b/openai/openai.go @@ -522,6 +522,20 @@ func FromChatRequest(r ChatCompletionRequest) (*api.ChatRequest, error) { } messages = append(messages, api.Message{Role: msg.Role, Images: []api.ImageData{img}}) + case "input_audio": + audioMap, ok := data["input_audio"].(map[string]any) + if !ok { + return nil, errors.New("invalid input_audio format") + } + b64Data, ok := audioMap["data"].(string) + if !ok { + return nil, errors.New("invalid input_audio format: missing data") + } + audioBytes, err := base64.StdEncoding.DecodeString(b64Data) + if err != nil { + return nil, fmt.Errorf("invalid input_audio base64 data: %w", err) + } + messages = append(messages, api.Message{Role: msg.Role, Images: []api.ImageData{audioBytes}}) default: return nil, errors.New("invalid message format") } @@ -824,6 +838,45 @@ func ToImageGenerationResponse(resp api.GenerateResponse) ImageGenerationRespons } } +// TranscriptionResponse is the response format for /v1/audio/transcriptions. +type TranscriptionResponse struct { + Text string `json:"text"` +} + +// TranscriptionRequest holds parsed fields from the multipart form. +type TranscriptionRequest struct { + Model string + AudioData []byte + ResponseFormat string // "json", "text", "verbose_json" + Language string + Prompt string +} + +// FromTranscriptionRequest converts a transcription request into a ChatRequest +// by wrapping the audio with a system prompt for transcription. +func FromTranscriptionRequest(r TranscriptionRequest) (*api.ChatRequest, error) { + systemPrompt := "Transcribe the following audio exactly as spoken. Output only the transcription text, nothing else." + if r.Language != "" { + systemPrompt += " The audio is in " + r.Language + "." + } + if r.Prompt != "" { + systemPrompt += " Context: " + r.Prompt + } + + stream := true + return &api.ChatRequest{ + Model: r.Model, + Messages: []api.Message{ + {Role: "system", Content: systemPrompt}, + {Role: "user", Content: "Transcribe this audio.", Images: []api.ImageData{r.AudioData}}, + }, + Stream: &stream, + Options: map[string]any{ + "temperature": 0, + }, + }, nil +} + // ImageEditRequest is an OpenAI-compatible image edit request. type ImageEditRequest struct { Model string `json:"model"` diff --git a/readline/readline.go b/readline/readline.go index 0113aa2ce..52d89b458 100644 --- a/readline/readline.go +++ b/readline/readline.go @@ -390,3 +390,48 @@ func (t *Terminal) Read() (rune, error) { } return r, nil } + +// SetRawModeOn enables raw terminal mode and keeps it on. +// Call SetRawModeOff to restore when done. +func (i *Instance) SetRawModeOn() error { + if i.Terminal.rawmode { + return nil + } + fd := os.Stdin.Fd() + termios, err := SetRawMode(fd) + if err != nil { + return err + } + i.Terminal.rawmode = true + i.Terminal.termios = termios + return nil +} + +// SetRawModeOff restores the terminal to its previous mode. +func (i *Instance) SetRawModeOff() { + if !i.Terminal.rawmode { + return + } + fd := os.Stdin.Fd() + //nolint:errcheck + UnsetRawMode(fd, i.Terminal.termios) + i.Terminal.rawmode = false +} + +// ReadRaw reads a single rune. If the terminal is already in raw mode +// (via SetRawModeOn), it reads directly. Otherwise it temporarily enters +// raw mode for the read. +func (i *Instance) ReadRaw() (rune, error) { + if !i.Terminal.rawmode { + fd := os.Stdin.Fd() + termios, err := SetRawMode(fd) + if err != nil { + return 0, err + } + defer func() { + //nolint:errcheck + UnsetRawMode(fd, termios) + }() + } + return i.Terminal.Read() +} diff --git a/runner/ollamarunner/runner.go b/runner/ollamarunner/runner.go index 49e4a5ed6..ccf646539 100644 --- a/runner/ollamarunner/runner.go +++ b/runner/ollamarunner/runner.go @@ -1258,6 +1258,12 @@ func (s *Server) loadModel() { panic(fmt.Errorf("failed to load model: %v", err)) } + if postLoader, ok := s.model.(model.PostLoader); ok { + if err := postLoader.PostLoad(); err != nil { + panic(fmt.Errorf("failed to finalize model initialization: %v", err)) + } + } + s.status = llm.ServerStatusReady s.ready.Done() } diff --git a/server/create.go b/server/create.go index 9797384fd..9c48b5186 100644 --- a/server/create.go +++ b/server/create.go @@ -141,7 +141,7 @@ func (s *Server) CreateHandler(c *gin.Context) { ch <- gin.H{"error": err.Error()} } - if err == nil && !remote && (config.Renderer == "" || config.Parser == "" || config.Requires == "") { + if err == nil && !remote && (config.Renderer == "" || config.Parser == "" || config.Requires == "" || len(config.Capabilities) == 0) { mf, mErr := manifest.ParseNamedManifest(fromName) if mErr == nil && mf.Config.Digest != "" { configPath, pErr := manifest.BlobsPath(mf.Config.Digest) @@ -158,6 +158,9 @@ func (s *Server) CreateHandler(c *gin.Context) { if config.Requires == "" { config.Requires = baseConfig.Requires } + if len(config.Capabilities) == 0 { + config.Capabilities = baseConfig.Capabilities + } } cfgFile.Close() } @@ -509,6 +512,24 @@ func createModel(r api.CreateRequest, name model.Name, baseLayers []*layerGGML, config.ModelType = cmp.Or(config.ModelType, format.HumanNumber(layer.GGML.KV().ParameterCount())) config.FileType = cmp.Or(config.FileType, layer.GGML.KV().FileType().String()) config.ModelFamilies = append(config.ModelFamilies, layer.GGML.KV().Architecture()) + + // Auto-detect renderer, parser, and stop tokens from GGUF architecture. + // TODO: abstract this into a registry/lookup table when multiple models + // need architecture-based renderer/parser/stop defaults. + if config.Renderer == "" || config.Parser == "" { + arch := layer.GGML.KV().Architecture() + switch arch { + case "gemma4": + config.Renderer = cmp.Or(config.Renderer, "gemma4") + config.Parser = cmp.Or(config.Parser, "gemma4") + if _, ok := r.Parameters["stop"]; !ok { + if r.Parameters == nil { + r.Parameters = make(map[string]any) + } + r.Parameters["stop"] = []string{""} + } + } + } } layers = append(layers, layer.Layer) } diff --git a/server/images.go b/server/images.go index 56cb4b898..a7fce62dc 100644 --- a/server/images.go +++ b/server/images.go @@ -39,6 +39,7 @@ var ( errCapabilityTools = errors.New("tools") errCapabilityInsert = errors.New("insert") errCapabilityVision = errors.New("vision") + errCapabilityAudio = errors.New("audio") errCapabilityEmbedding = errors.New("embedding") errCapabilityThinking = errors.New("thinking") errCapabilityImage = errors.New("image generation") @@ -93,14 +94,26 @@ func (m *Model) Capabilities() []model.Capability { if f.KeyValue("vision.block_count").Valid() { capabilities = append(capabilities, model.CapabilityVision) } + if f.KeyValue("audio.block_count").Valid() { + capabilities = append(capabilities, model.CapabilityAudio) + } } else { slog.Error("couldn't open model file", "error", err) } - } else if len(m.Config.Capabilities) > 0 { + } + + // Also include capabilities from the model config (e.g. vision capability + // set during creation for MLX/safetensors models). + if len(m.Config.Capabilities) > 0 { for _, c := range m.Config.Capabilities { - capabilities = append(capabilities, model.Capability(c)) + cap := model.Capability(c) + if !slices.Contains(capabilities, cap) { + capabilities = append(capabilities, cap) + } } - } else { + } + + if len(capabilities) == 0 { slog.Warn("unknown capabilities for model", "model", m.Name) } @@ -141,6 +154,14 @@ func (m *Model) Capabilities() []model.Capability { capabilities = append(capabilities, model.CapabilityThinking) } + // Temporary workaround — suppress vision/audio for gemma4 MLX models + // until multimodal runtime pipeline lands. Remove when imageproc.go is wired up. + if m.Config.ModelFormat == "safetensors" && m.Config.Renderer == "gemma4" { + capabilities = slices.DeleteFunc(capabilities, func(c model.Capability) bool { + return c == model.CapabilityVision || c == "audio" + }) + } + return capabilities } @@ -156,6 +177,7 @@ func (m *Model) CheckCapabilities(want ...model.Capability) error { model.CapabilityTools: errCapabilityTools, model.CapabilityInsert: errCapabilityInsert, model.CapabilityVision: errCapabilityVision, + model.CapabilityAudio: errCapabilityAudio, model.CapabilityEmbedding: errCapabilityEmbedding, model.CapabilityThinking: errCapabilityThinking, model.CapabilityImage: errCapabilityImage, diff --git a/server/quantization.go b/server/quantization.go index 56d882e84..ee70b5fc0 100644 --- a/server/quantization.go +++ b/server/quantization.go @@ -153,7 +153,16 @@ func getTensorNewType(kv fsggml.KV, qs *quantizeState, newType fsggml.TensorType // MLA tensors need higher precision to avoid quality degradation newType = fsggml.TensorTypeQ8_0 } else if strings.Contains(name, "ffn_down") { - iLayer := qs.iFfnDown + // For MoE models, ffn_down.weight (dense) and ffn_down_exps.weight (expert) both + // exist per layer and should get the same useMoreBits treatment. Dense sorts before + // expert alphabetically, so dense increments the counter and expert uses counter-1. + var iLayer int + if strings.Contains(name, "_exps") { + iLayer = max(0, qs.iFfnDown-1) + } else { + iLayer = qs.iFfnDown + qs.iFfnDown++ + } n_layer := qs.nFfnDown if ftype == fsggml.FileTypeQ4_K_M { if useMoreBits(iLayer, n_layer) { @@ -162,7 +171,6 @@ func getTensorNewType(kv fsggml.KV, qs *quantizeState, newType fsggml.TensorType } else if ftype == fsggml.FileTypeQ4_K_S && iLayer < n_layer/8 { newType = fsggml.TensorTypeQ5_K } - qs.iFfnDown++ } else if strings.Contains(name, "attn_output.weight") { if nExperts == 8 { if ftype == fsggml.FileTypeQ4_K_S || ftype == fsggml.FileTypeQ4_K_M { @@ -255,8 +263,9 @@ func newType(t *fsggml.Tensor, kv fsggml.KV, qs *quantizeState, ftype fsggml.Fil name := t.Name quantize := strings.HasSuffix(name, "weight") - // don't quantize vision encoder tensors (named with "v." prefix) + // don't quantize vision or audio encoder tensors quantize = quantize && !strings.HasPrefix(name, "v.") + quantize = quantize && !strings.HasPrefix(name, "a.") quantize = quantize && !strings.Contains(name, "mm.") // quantize only 2D and 3D tensors (experts) diff --git a/server/routes.go b/server/routes.go index 28384ed42..8b96e4192 100644 --- a/server/routes.go +++ b/server/routes.go @@ -1718,6 +1718,8 @@ func (s *Server) GenerateRoutes(rc *ollama.Registry) (http.Handler, error) { // OpenAI-compatible image generation endpoints r.POST("/v1/images/generations", cloudPassthroughMiddleware(cloudErrRemoteInferenceUnavailable), middleware.ImageGenerationsMiddleware(), s.GenerateHandler) r.POST("/v1/images/edits", cloudPassthroughMiddleware(cloudErrRemoteInferenceUnavailable), middleware.ImageEditsMiddleware(), s.GenerateHandler) + // OpenAI-compatible audio endpoint + r.POST("/v1/audio/transcriptions", middleware.TranscriptionMiddleware(), s.ChatHandler) // Inference (Anthropic compatibility) r.POST("/v1/messages", s.withInferenceRequestLogging("/v1/messages", cloudPassthroughMiddleware(cloudErrRemoteInferenceUnavailable), middleware.AnthropicMessagesMiddleware(), s.ChatHandler)...) diff --git a/types/model/capability.go b/types/model/capability.go index aeac37961..4cf628c09 100644 --- a/types/model/capability.go +++ b/types/model/capability.go @@ -10,6 +10,7 @@ const ( CapabilityEmbedding = Capability("embedding") CapabilityThinking = Capability("thinking") CapabilityImage = Capability("image") + CapabilityAudio = Capability("audio") ) func (c Capability) String() string {