{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.6.6","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":16880,"databundleVersionId":858837,"sourceType":"competition"}],"dockerImageVersionId":29845,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"\n\n#%%writefile skin_texture_analyzer.py\nimport os\nimport cv2\nimport numpy as np\nimport pandas as pd\nimport tensorflow as tf\nfrom sklearn.model_selection import train_test_split\nfrom skimage.feature import local_binary_pattern\nfrom scipy.stats import entropy, skew, kurtosis\nfrom datetime import datetime\nimport tensorflow as tf\nfrom tensorflow.keras.applications import ResNet50\nfrom tensorflow.keras.models import Model\nfrom tensorflow.keras.layers import Dense, Dropout, GlobalAveragePooling2D, Input, Concatenate\nfrom tensorflow.keras.optimizers import Adam\nfrom tqdm import tqdm\nimport matplotlib.pyplot as plt\nfrom skin_texture_analyzer import SkinTextureAnalyzer\nTRAIN_DIR = '/kaggle/input/deepfake-detection-challenge/test_videos'\nLABELS_FILE = '/kaggle/input/deepfake-detection-challenge/sample_submission.csv'\nFRAMES_TO_EXTRACT = 10\nIMG_SIZE = 224\nBATCH_SIZE = 32\nEPOCHS = 50\nMODEL_SAVE_PATH = '/kaggle/working/deepfake_detector_model'\nFEATURES_DIR = '/kaggle/working/features'","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-02-26T21:10:33.768513Z","iopub.execute_input":"2025-02-26T21:10:33.768828Z","iopub.status.idle":"2025-02-26T21:10:33.775078Z","shell.execute_reply.started":"2025-02-26T21:10:33.768771Z","shell.execute_reply":"2025-02-26T21:10:33.7743Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Create directories if they don't exist\nos.makedirs(FEATURES_DIR, exist_ok=True)\n\n# Завагтажуєм мітки\nlabels_df = pd.read_csv(LABELS_FILE)\nprint(f\"Loaded {len(labels_df)} video labels\")\n\ndef extract_frames(video_path, num_frames=FRAMES_TO_EXTRACT):\n    \"\"\"Витягнути кадри з відеофайлу\"\"\"\n    frames = []\n    try:\n        cap = cv2.VideoCapture(video_path)\n        total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))\n        if total_frames <= 0:\n            return frames\n            \n        # Розрахуйте кадри, які потрібно пропустити, щоб отримати рівномірно розподілені кадри\n        skip_frames = max(1, total_frames // num_frames)\n        \n        # Витягнути кадри\n        frame_indices = []\n        for i in range(0, total_frames, skip_frames):\n            if len(frame_indices) < num_frames:\n                frame_indices.append(i)\n            else:\n                break\n                \n        for idx in frame_indices:\n            cap.set(cv2.CAP_PROP_POS_FRAMES, idx)\n            ret, frame = cap.read()\n            if ret:\n                # розмір кадру\n                frame = cv2.resize(frame, (IMG_SIZE, IMG_SIZE))\n                frames.append(frame)\n        \n        cap.release()\n    except Exception as e:\n        print(f\"Error extracting frames from {video_path}: {e}\")\n    \n    return frames\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-02-26T21:10:36.306075Z","iopub.execute_input":"2025-02-26T21:10:36.306349Z","iopub.status.idle":"2025-02-26T21:10:36.319309Z","shell.execute_reply.started":"2025-02-26T21:10:36.306311Z","shell.execute_reply":"2025-02-26T21:10:36.318348Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def extract_texture_features(video_id, frames, analyzer):\n    \"\"\"Вилучення текстурних елементів з кадрів за допомогою SkinTextureAnalyzer\"\"\"\n    features_list = []\n    \n    for i, frame in enumerate(frames):\n        results = analyzer.analyze_skin_texture(frame)\n        if results is None:  # Обличчя не виявлено\n            continue\n            \n        # Створіть вектор характеристик\n        feature_dict = {\n            'video_id': video_id,\n            'frame_idx': i,\n            'naturalness_score': results['naturalness_score'],\n            'smoothness': results['smoothness'],\n            'uniformity': results['uniformity'],\n            # Особливості LBP\n            'lbp_entropy': results['lbp_features']['lbp_entropy'],\n            'lbp_variance': results['lbp_features']['lbp_variance'],\n            'lbp_uniformity': results['lbp_features']['lbp_uniformity'],\n            # Особливості градієнта\n            'gradient_mean': results['gradient_features']['gradient_mean'],\n            'gradient_std': results['gradient_features']['gradient_std'],\n            'gradient_direction_entropy': results['gradient_features']['gradient_direction_entropy'],\n            # Статистичні особливості\n            'texture_mean': results['statistical_features']['mean'],\n            'texture_std': results['statistical_features']['std'],\n            'texture_skewness': results['statistical_features']['skewness'],\n            'texture_kurtosis': results['statistical_features']['kurtosis'],\n            # Особливості переходу кольорів - згладжені\n            'red_mean_gradient': results['color_transitions']['red']['mean_gradient'],\n            'red_max_gradient': results['color_transitions']['red']['max_gradient'],\n            'green_mean_gradient': results['color_transitions']['green']['mean_gradient'],\n            'green_max_gradient': results['color_transitions']['green']['max_gradient'],\n            'blue_mean_gradient': results['color_transitions']['blue']['mean_gradient'],\n            'blue_max_gradient': results['color_transitions']['blue']['max_gradient']\n        }\n        \n        features_list.append(feature_dict)\n    \n    if not features_list:\n        return None\n        \n # Перетворення в DataFrame\n    features_df = pd.DataFrame(features_list)\n    \n    # Агрегувати функції в різних кадрах\n    # Select only numeric columns for aggregation\n    numeric_cols = features_df.select_dtypes(include=['number']).columns\n    numeric_cols = [col for col in numeric_cols if col not in ['video_id', 'frame_idx']]\n    \n    # Calculate aggregations\n    mean_features = features_df[numeric_cols].mean().to_dict()\n    std_features = features_df[numeric_cols].std().fillna(0).to_dict()\n    min_features = features_df[numeric_cols].min().to_dict()\n    max_features = features_df[numeric_cols].max().to_dict()\n    \n    # Create a clean output with proper naming\n    agg_features = {}\n    for col in numeric_cols:\n        agg_features[f'{col}_mean'] = mean_features[col]\n        agg_features[f'{col}_std'] = std_features[col]\n        agg_features[f'{col}_min'] = min_features[col]\n        agg_features[f'{col}_max'] = max_features[col]\n    \n    # Add video_id back\n    agg_features['video_id'] = video_id\n    \n    # Return as a DataFrame \n    return pd.DataFrame([agg_features])\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-02-26T21:46:48.695801Z","iopub.execute_input":"2025-02-26T21:46:48.696084Z","iopub.status.idle":"2025-02-26T21:46:48.708931Z","shell.execute_reply.started":"2025-02-26T21:46:48.696044Z","shell.execute_reply":"2025-02-26T21:46:48.708014Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def build_hybrid_model():\n    \"\"\"\n Побудуйте гібридну модель, яка поєднує:\n    1. Функції CNN від попередньо навченого ResNet50\n    2. Інженерні властивості текстури від SkinTextureAnalyzer\n    \"\"\"\n    # CNN branch\n    cnn_input = Input(shape=(IMG_SIZE, IMG_SIZE, 3))\n    base_model = ResNet50(weights='imagenet', include_top=False, input_tensor=cnn_input)\n    \n    # Заморозити шари базової моделі\n    for layer in base_model.layers:\n        layer.trainable = False\n    \n    cnn_features = base_model.output\n    cnn_features = GlobalAveragePooling2D()(cnn_features)\n    cnn_features = Dropout(0.5)(cnn_features)\n    cnn_branch = Dense(256, activation='relu')(cnn_features)\n    \n    # Гілка властивостей текстури\n    texture_input = Input(shape=(84,))  # 21 ознака × 4 агрегації (середнє, середньоквадратичне, мінімальне, максимальне)\n    texture_branch = Dense(128, activation='relu')(texture_input)\n    texture_branch = Dropout(0.3)(texture_branch)\n    texture_branch = Dense(64, activation='relu')(texture_branch)\n    \n    # Об'єднати гілки\n    combined = Concatenate()([cnn_branch, texture_branch])\n    combined = Dense(128, activation='relu')(combined)\n    combined = Dropout(0.5)(combined)\n    combined = Dense(64, activation='relu')(combined)\n    combined = Dropout(0.3)(combined)\n    output = Dense(1, activation='sigmoid')(combined)\n    \n    model = Model(inputs=[cnn_input, texture_input], outputs=output)\n    model.compile(\n        optimizer=Adam(learning_rate=0.0001),\n        loss='binary_crossentropy',\n        metrics=['accuracy', tf.keras.metrics.AUC()]\n    )\n    \n    return model\n\ndef process_videos_to_features():\n    \"\"\"Process all videos and extract features\"\"\"\n    analyzer = SkinTextureAnalyzer()\n    all_features = []\n    \n    print(\"Extracting features from videos...\")\n    for idx, row in tqdm(labels_df.iterrows(), total=len(labels_df)):\n        video_id = row['filename'].split('.')[0]\n        video_path = os.path.join(TRAIN_DIR, row['filename'])\n        label = row['label']\n        \n        # Check if video file exists\n        if not os.path.exists(video_path):\n            print(f\"Video file not found: {video_path}\")\n            continue\n            \n        # Extract frames\n        frames = extract_frames(video_path)\n        if not frames:\n            print(f\"No frames extracted from {video_path}\")\n            continue\n            \n        # Extract texture features\n        features = extract_texture_features(video_id, frames, analyzer)\n        if features is not None:\n            features['label'] = label\n            all_features.append(features)\n            \n        # Save frames for CNN training later\n        for i, frame in enumerate(frames[:min(5, len(frames))]):  # Save up to 5 frames per video\n            frame_dir = os.path.join(FEATURES_DIR, 'frames', str(int(label)))\n            os.makedirs(frame_dir, exist_ok=True)\n            cv2.imwrite(os.path.join(frame_dir, f\"{video_id}_{i}.jpg\"), frame)\n    \n    # Combine all features\n    if all_features:\n        combined_features = pd.concat(all_features, ignore_index=True)\n        combined_features.to_csv(os.path.join(FEATURES_DIR, 'texture_features.csv'), index=False)\n        print(f\"Saved features for {len(combined_features)} videos\")\n        return combined_features\n    else:\n        print(\"No features extracted!\")\n        return None\n\ndef train_texture_model(features_df):\n    \"\"\"Train a model using the extracted texture features\"\"\"\n    # Make a copy to avoid modifying the original\n    features_df_copy = features_df.copy()\n    \n    # Drop video_id and prepare X, y\n    X = features_df_copy.drop(['video_id', 'label'], axis=1)\n    y = features_df_copy['label']\n    \n    # Verify all data is numeric\n    for col in X.columns:\n        if X[col].dtype == 'object':\n            print(f\"Column {col} has non-numeric data: {X[col].unique()[:5]}\")\n            X[col] = pd.to_numeric(X[col], errors='coerce')\n    \n    # Fill any NaN values that might have been created\n    X = X.fillna(0)\n    \n    # Now convert to numpy arrays\n    X_values = X.values.astype(np.float32)\n    y_values = y.values.astype(np.float32)\n    \n    # Split data\n    X_train, X_test, y_train, y_test = train_test_split(X_values, y_values, test_size=0.2, random_state=42, stratify=y_values)\n    \n    print(f\"Training texture model with {X_train.shape[1]} features\")\n    print(f\"X_train shape: {X_train.shape}, y_train shape: {y_train.shape}\")\n    print(f\"X_train dtype: {X_train.dtype}, y_train dtype: {y_train.dtype}\")\n    \n    # Train a simple model\n    model = tf.keras.Sequential([\n        Dense(128, activation='relu', input_shape=(X_train.shape[1],)),\n        Dropout(0.4),\n        Dense(64, activation='relu'),\n        Dropout(0.3),\n        Dense(32, activation='relu'),\n        Dense(1, activation='sigmoid')\n    ])\n    \n    model.compile(\n        optimizer=Adam(learning_rate=0.001),\n        loss='binary_crossentropy',\n        metrics=['accuracy', tf.keras.metrics.AUC()]\n    )\n    \n    # Train model\n    history = model.fit(\n        X_train, y_train,\n        validation_data=(X_test, y_test),\n        epochs=30,\n        batch_size=32,\n        callbacks=[\n            tf.keras.callbacks.EarlyStopping(monitor='val_loss', patience=5, restore_best_weights=True),\n            tf.keras.callbacks.ReduceLROnPlateau(monitor='val_loss', factor=0.5, patience=3)\n        ]\n    )\n    \n    # Evaluate\n    test_loss, test_acc, test_auc = model.evaluate(X_test, y_test)\n    print(f\"Test accuracy: {test_acc:.4f}, AUC: {test_auc:.4f}\")\n    \n    # Save model\n    texture_model_path = os.path.join(MODEL_SAVE_PATH, 'texture_model')\n    model.save(texture_model_path)\n    print(f\"Saved texture model to {texture_model_path}\")\n    \n    # Plot training history\n    plt.figure(figsize=(12, 4))\n    plt.subplot(1, 2, 1)\n    plt.plot(history.history['loss'], label='Training Loss')\n    plt.plot(history.history['val_loss'], label='Validation Loss')\n    plt.title('Loss')\n    plt.legend()\n    \n    plt.subplot(1, 2, 2)\n    plt.plot(history.history['accuracy'], label='Training Accuracy')\n    plt.plot(history.history['val_accuracy'], label='Validation Accuracy')\n    plt.title('Accuracy')\n    plt.legend()\n    \n    plt.savefig(os.path.join(FEATURES_DIR, 'training_history.png'))\n    plt.close()\n    \n    return model\n\nclass DeepfakeDataGenerator(tf.keras.utils.Sequence):\n    \"\"\"Data generator for the hybrid model\"\"\"\n    def __init__(self, frame_paths, texture_features, labels, batch_size=32, is_training=True):\n        self.frame_paths = frame_paths\n        self.texture_features = texture_features\n        self.labels = labels\n        self.batch_size = batch_size\n        self.is_training = is_training\n        self.indexes = np.arange(len(self.frame_paths))\n        if is_training:\n            np.random.shuffle(self.indexes)\n    \n    def __len__(self):\n        return int(np.ceil(len(self.frame_paths) / self.batch_size))\n    \n    def __getitem__(self, idx):\n        batch_indexes = self.indexes[idx * self.batch_size:(idx + 1) * self.batch_size]\n        \n        batch_frames = []\n        batch_features = []\n        batch_labels = []\n        \n        for i in batch_indexes:\n            # Load and preprocess image\n            img = cv2.imread(self.frame_paths[i])\n            img = cv2.resize(img, (IMG_SIZE, IMG_SIZE))\n            img = img / 255.0  # Normalize\n            \n            # Get corresponding texture features\n            features = self.texture_features[i]\n            \n            batch_frames.append(img)\n            batch_features.append(features)\n            batch_labels.append(self.labels[i])\n        \n        return [np.array(batch_frames), np.array(batch_features)], np.array(batch_labels)\n    \n    def on_epoch_end(self):\n        if self.is_training:\n            np.random.shuffle(self.indexes)\n\ndef prepare_hybrid_training_data(features_df):\n    \"\"\"Prepare data for hybrid model training\"\"\"\n    # Get frame paths\n    frame_dir = os.path.join(FEATURES_DIR, 'frames')\n    frame_paths = []\n    texture_features = []\n    labels = []\n    \n    for label in ['0', '1']:\n        label_dir = os.path.join(frame_dir, label)\n        if not os.path.exists(label_dir):\n            continue\n            \n        for filename in os.listdir(label_dir):\n            if filename.endswith('.jpg'):\n                video_id = filename.split('_')[0]\n                \n                # Get corresponding texture features\n                video_features = features_df[features_df['video_id'] == video_id]\n                if len(video_features) == 0:\n                    continue\n                    \n                # Use mean features for this video\n                feature_cols = [col for col in features_df.columns \n                                if col not in ['video_id', 'label']]\n                video_feature_vector = video_features[feature_cols].values[0]\n                \n                frame_paths.append(os.path.join(label_dir, filename))\n                texture_features.append(video_feature_vector)\n                labels.append(int(label))\n    \n    print(f\"Prepared {len(frame_paths)} samples for hybrid model training\")\n    \n    # Split data\n    train_idx, test_idx = train_test_split(\n        np.arange(len(frame_paths)), \n        test_size=0.2, \n        random_state=42,\n        stratify=labels\n    )\n    \n    train_frames = [frame_paths[i] for i in train_idx]\n    train_features = [texture_features[i] for i in train_idx]\n    train_labels = [labels[i] for i in train_idx]\n    \n    test_frames = [frame_paths[i] for i in test_idx]\n    test_features = [texture_features[i] for i in test_idx]\n    test_labels = [labels[i] for i in test_idx]\n    \n    # Create data generators\n    train_gen = DeepfakeDataGenerator(train_frames, train_features, train_labels, batch_size=BATCH_SIZE)\n    test_gen = DeepfakeDataGenerator(test_frames, test_features, test_labels, batch_size=BATCH_SIZE, is_training=False)\n    \n    return train_gen, test_gen\n\ndef train_hybrid_model(train_gen, test_gen):\n    \"\"\"Train the hybrid model\"\"\"\n    model = build_hybrid_model()\n    \n    # Callbacks\n    callbacks = [\n        tf.keras.callbacks.ModelCheckpoint(\n            filepath=os.path.join(MODEL_SAVE_PATH, 'hybrid_model_best.h5'),\n            monitor='val_accuracy',\n            save_best_only=True,\n            mode='max'\n        ),\n        tf.keras.callbacks.EarlyStopping(\n            monitor='val_loss',\n            patience=7,\n            restore_best_weights=True\n        ),\n        tf.keras.callbacks.ReduceLROnPlateau(\n            monitor='val_loss',\n            factor=0.5,\n            patience=3\n        ),\n        tf.keras.callbacks.TensorBoard(\n            log_dir=os.path.join(MODEL_SAVE_PATH, 'logs'),\n            histogram_freq=1\n        )\n    ]\n    \n    # Train\n    history = model.fit(\n        train_gen,\n        validation_data=test_gen,\n        epochs=EPOCHS,\n        callbacks=callbacks\n    )\n    \n    # Save final model\n    model.save(os.path.join(MODEL_SAVE_PATH, 'hybrid_model_final'))\n    \n    # Plot training history\n    plt.figure(figsize=(12, 4))\n    plt.subplot(1, 2, 1)\n    plt.plot(history.history['loss'], label='Training Loss')\n    plt.plot(history.history['val_loss'], label='Validation Loss')\n    plt.title('Loss')\n    plt.legend()\n    \n    plt.subplot(1, 2, 2)\n    plt.plot(history.history['accuracy'], label='Training Accuracy')\n    plt.plot(history.history['val_accuracy'], label='Validation Accuracy')\n    plt.title('Accuracy')\n    plt.legend()\n    \n    plt.savefig(os.path.join(MODEL_SAVE_PATH, 'hybrid_model_history.png'))\n    plt.close()\n    \n    return model\n\ndef analyze_feature_importance(model, feature_names):\n    \"\"\"Analyze which features are most important for classification\"\"\"\n    # For basic feature importance, we'll use the weights of the first Dense layer\n    if len(model.layers) < 2:\n        print(\"Model doesn't have enough layers for feature importance analysis\")\n        return\n    \n    # Get weights of the first Dense layer (for texture features)\n    layer_name = 'dense_1'  # Adjust based on your model architecture\n    for i, layer in enumerate(model.layers):\n        if layer.name == layer_name:\n            weights = layer.get_weights()[0]\n            bias = layer.get_weights()[1]\n            \n            # Calculate absolute importance\n            importance = np.mean(np.abs(weights), axis=1)\n            \n            # Create DataFrame\n            importance_df = pd.DataFrame({\n                'Feature': feature_names,\n                'Importance': importance\n            })\n            \n            importance_df = importance_df.sort_values('Importance', ascending=False)\n            \n            # Plot\n            plt.figure(figsize=(12, 8))\n            plt.barh(importance_df['Feature'][:20], importance_df['Importance'][:20])\n            plt.title('Top 20 Feature Importance')\n            plt.xlabel('Mean Absolute Weight')\n            plt.tight_layout()\n            plt.savefig(os.path.join(FEATURES_DIR, 'feature_importance.png'))\n            plt.close()\n            \n            # Save to CSV\n            importance_df.to_csv(os.path.join(FEATURES_DIR, 'feature_importance.csv'), index=False)\n            return importance_df\n    \n    print(f\"Layer {layer_name} not found in model\")\n    return None\n\ndef predict_on_video(video_path, model, analyzer):\n    \"\"\"Make predictions on a single video\"\"\"\n    # Extract frames\n    frames = extract_frames(video_path)\n    if not frames:\n        return None\n    \n    # Extract texture features\n    video_id = os.path.basename(video_path).split('.')[0]\n    features = extract_texture_features(video_id, frames, analyzer)\n    \n    if features is None:\n        return None\n    \n    # Prepare data for prediction\n    feature_cols = [col for col in features.columns if col != 'video_id']\n    X_texture = features[feature_cols].values\n    \n    # Process frames for CNN\n    X_frames = np.array([cv2.resize(frame, (IMG_SIZE, IMG_SIZE)) / 255.0 for frame in frames])\n    \n    # Make predictions\n    predictions = []\n    for i in range(len(X_frames)):\n        pred = model.predict([np.expand_dims(X_frames[i], axis=0), np.expand_dims(X_texture[0], axis=0)])\n        predictions.append(pred[0][0])\n    \n    # Aggregate predictions\n    mean_pred = np.mean(predictions)\n    max_pred = np.max(predictions)\n    min_pred = np.min(predictions)\n    \n    return {\n        'video_id': video_id,\n        'mean_pred': mean_pred,\n        'max_pred': max_pred,\n        'min_pred': min_pred,\n        'is_fake': mean_pred > 0.5,\n        'confidence': abs(mean_pred - 0.5) * 2  # Scale to 0-1\n    }\n\ndef main():\n    print(\"Starting deepfake detection pipeline...\")\n    start_time = datetime.now()\n    \n    # Process videos and extract features\n    features_df = process_videos_to_features()\n    \n    if features_df is None:\n        print(\"No features extracted. Exiting.\")\n        return\n    \n    # Train texture-based model\n    texture_model = train_texture_model(features_df)\n    \n    # Prepare data for hybrid model\n    train_gen, test_gen = prepare_hybrid_training_data(features_df)\n    \n    # Train hybrid model\n    hybrid_model = train_hybrid_model(train_gen, test_gen)\n    \n    # Analyze feature importance\n    feature_names = [col for col in features_df.columns if col not in ['video_id', 'label']]\n    importance_df = analyze_feature_importance(texture_model, feature_names)\n    \n    end_time = datetime.now()\n    print(f\"Pipeline completed in {end_time - start_time}\")\n    \n    # Test on specific videos if needed\n    test_video = os.path.join(TRAIN_DIR, labels_df.iloc[0]['filename'])\n    analyzer = SkinTextureAnalyzer()\n    result = predict_on_video(test_video, hybrid_model, analyzer)\n    \n    if result:\n        print(f\"Prediction for {result['video_id']}: {'FAKE' if result['is_fake'] else 'REAL'}\")\n        print(f\"Confidence: {result['confidence']:.4f}\")\n\nif __name__ == \"__main__\":\n    main()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-02-26T21:49:02.608186Z","iopub.execute_input":"2025-02-26T21:49:02.60863Z","iopub.status.idle":"2025-02-26T22:18:05.544634Z","shell.execute_reply.started":"2025-02-26T21:49:02.608446Z","shell.execute_reply":"2025-02-26T22:18:05.543067Z"}},"outputs":[],"execution_count":null}]}