From ccfd0f91885279b24578fc858f77ecaf8c643450 Mon Sep 17 00:00:00 2001 From: Luciano Gervasoni Date: Wed, 30 Apr 2025 15:50:54 +0200 Subject: [PATCH] Schools NL, Ghost post utils, nude + age detection --- .gitignore | 6 +- app_cv/Demo.ipynb | 157 ++++++++++++++++ app_cv/README.md | 17 ++ app_cv/Server.ipynb | 35 ++++ app_cv/app.py | 48 +++++ app_cv/cv_processor.py | 132 +++++++++++++ app_cv/imgs/img_1p.jpg | Bin 0 -> 36154 bytes app_cv/imgs/img_nude.jpg | Bin 0 -> 29789 bytes utils/Ghost-Posts.ipynb | 120 +++++++++--- utils/Schools-NL.ipynb | 390 +++++++++++++++++---------------------- utils/Summary.ipynb | 182 ++++++++++++++++++ 11 files changed, 841 insertions(+), 246 deletions(-) create mode 100644 app_cv/Demo.ipynb create mode 100644 app_cv/README.md create mode 100644 app_cv/Server.ipynb create mode 100644 app_cv/app.py create mode 100644 app_cv/cv_processor.py create mode 100644 app_cv/imgs/img_1p.jpg create mode 100644 app_cv/imgs/img_nude.jpg create mode 100644 utils/Summary.ipynb diff --git a/.gitignore b/.gitignore index 99fcf64..ea65850 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,8 @@ __pycache__/ **/credentials.py logs/ postgres/ -docker_data/ \ No newline at end of file +docker_data/ +**/*.pt +**/*.pth +**/*.tar +**/*.onnx diff --git a/app_cv/Demo.ipynb b/app_cv/Demo.ipynb new file mode 100644 index 0000000..28a2ecc --- /dev/null +++ b/app_cv/Demo.ipynb @@ -0,0 +1,157 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import base64\n", + "import json\n", + "import requests\n", + "import io\n", + "import numpy as np\n", + "import PIL.Image\n", + "import cv2\n", + "from pprint import pprint\n", + "\n", + "def process_image(path_img):\n", + " with open(path_img, \"rb\") as image_file:\n", + " encoded_string = base64.b64encode(image_file.read()).decode('utf-8')\n", + " response = requests.post(\n", + " 'http://localhost:5000/process',\n", + " headers={'Content-Type': 'application/json'},\n", + " data=json.dumps({'image': encoded_string})\n", + " )\n", + " response_dict = response.json()\n", + " pprint(response_dict)\n", + " # Decode\n", + " image_bytes = base64.b64decode(response_dict.get(\"image_b64\"))\n", + " img_array = np.frombuffer(io.BytesIO(image_bytes).getvalue(), dtype=np.uint8)\n", + " img_bgr = cv2.imdecode(img_array, cv2.IMREAD_COLOR)\n", + " img_rgb = img_bgr[:, :, ::-1]\n", + " return img_rgb" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "path_img = \"imgs/img_1p.jpg\"\n", + "PIL.Image.fromarray( process_image(path_img) )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "path_img = \"imgs/img_nude.jpg\"\n", + "PIL.Image.fromarray( process_image(path_img) )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "'''\n", + "# !git clone https://github.com/wildchlamydia/mivolo\n", + "# !pip install ultralytics yt_dlp pandas scipy timm==0.8.13.dev0\n", + "# !pip install ./mivolo\n", + "\n", + "!python mivolo/demo.py \\\n", + " --input \"face_data/sample_image.jpg\" \\\n", + " --output \"output\" \\\n", + " --detector-weights \"mivolo/pretrained/yolov8x_person_face.pt\" \\\n", + " --checkpoint \"mivolo/pretrained/model_imdb_cross_person_4.22_99.46.pth.tar\" \\\n", + " --device \"cpu\" \\\n", + " --draw\n", + "'''\n", + "\n", + "'''\n", + "# !git clone https://github.com/Kartik-3004/facexformer.git\n", + "# !pip install huggingface_hub torch torchvision torchaudio opencv-python facenet_pytorch\n", + "from huggingface_hub import hf_hub_download\n", + "hf_hub_download(repo_id=\"kartiknarayan/facexformer\", filename=\"ckpts/model.pt\", local_dir=\"./facexformer\")\n", + "\n", + "!python facexformer/inference.py \\\n", + " --model_path facexformer/ckpts/model.pt \\\n", + " --image_path face_data/sample_image.jpg \\\n", + " --results_path face_data \\\n", + " --task parsing\n", + " x\n", + "!python facexformer/inference.py \\\n", + " --model_path facexformer/ckpts/model.pt \\\n", + " --image_path face_data/face.png \\\n", + " --results_path face_data \\\n", + " --task landmarks\n", + "\n", + "!python facexformer/inference.py \\\n", + " --model_path facexformer/ckpts/model.pt \\\n", + " --image_path face_data/face.png \\\n", + " --results_path face_data \\\n", + " --task headpose\n", + "\n", + "!python facexformer/inference.py \\\n", + " --model_path facexformer/ckpts/model.pt \\\n", + " --image_path face_data/face.png \\\n", + " --results_path face_data \\\n", + " --task attributes\n", + "\n", + "!python facexformer/inference.py \\\n", + " --model_path facexformer/ckpts/model.pt \\\n", + " --image_path face_data/face.png \\\n", + " --results_path face_data \\\n", + " --task age_gender_race\n", + "'''" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "matitos_cv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.9" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/app_cv/README.md b/app_cv/README.md new file mode 100644 index 0000000..792d809 --- /dev/null +++ b/app_cv/README.md @@ -0,0 +1,17 @@ +# Requirements + +``` +pip install git+https://github.com/wildchlamydia/mivolo.git "nudenet>=3.4.2" +``` + +- Download checkpoints + - https://github.com/wildchlamydia/mivolo + - models/mivolo/model_imdb_cross_person_4.22_99.46.pth.tar + - models/mivolo/yolov8x_person_face.pt + - https://github.com/notAI-tech/NudeNet?tab=readme-ov-file#available-models + - models/nude_detector/640m.onnx + + +# TODO + +- Client side inference: https://github.com/notAI-tech/NudeNet/tree/v3/in_browser diff --git a/app_cv/Server.ipynb b/app_cv/Server.ipynb new file mode 100644 index 0000000..43d2eda --- /dev/null +++ b/app_cv/Server.ipynb @@ -0,0 +1,35 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# !pip install flask\n", + "!python app.py" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "matitos_cv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.9" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/app_cv/app.py b/app_cv/app.py new file mode 100644 index 0000000..c73d1e3 --- /dev/null +++ b/app_cv/app.py @@ -0,0 +1,48 @@ +from flask import Flask, request, jsonify +import base64 +import io +import cv2 +import traceback +import os +import logging +logging.basicConfig(level=logging.DEBUG, format='%(asctime)s [%(levelname)s] %(message)s', handlers=[logging.StreamHandler()]) +from cv_processor import process + +app = Flask(__name__) + +@app.route('/process', methods=['POST']) +def process_image(): + logging.info("POST /process") + # Json + data = request.get_json() + # Valid data? + if not data or 'image' not in data: + return jsonify({"error": "No image data provided"}), 400 + + try: + image_data = data['image'] + + # Decode base64 string + image_bytes = base64.b64decode(image_data) + image_stream = io.BytesIO(image_bytes) + + # Process the image + results = process(image_stream) + + # Encode processed image to base64 + _, buffer = cv2.imencode('.jpg', results.get("image"), [cv2.IMWRITE_JPEG_QUALITY, 100]) + processed_image_base64 = base64.b64encode(buffer).decode('utf-8') + + # Update image with base64 encoded + results["image_b64"] = processed_image_base64 + # Pop image (not serializable) + results.pop("image") + # Jsonify + return jsonify(results) + + except Exception as e: + logging.warning("Exception: {}".format(traceback.format_exc())) + return jsonify({"error": traceback.format_exc()}), 400 + +if __name__ == '__main__': + app.run(debug=os.getenv("DEBUG_MODE", False)) diff --git a/app_cv/cv_processor.py b/app_cv/cv_processor.py new file mode 100644 index 0000000..445f114 --- /dev/null +++ b/app_cv/cv_processor.py @@ -0,0 +1,132 @@ +import cv2 +import numpy as np +import logging +logging.basicConfig(level=logging.DEBUG, format='%(asctime)s [%(levelname)s] %(message)s', handlers=[logging.StreamHandler()]) + +# Age +from mivolo.predictor import Predictor +import argparse +# Nudity +from nudenet import NudeDetector + + +class CV(): + def __init__(self): + args = argparse.ArgumentParser() + args.add_argument("--device", type=str, default="cpu") + args.add_argument("--checkpoint", default="models/mivolo/model_imdb_cross_person_4.22_99.46.pth.tar") + args.add_argument("--detector_weights", default="models/mivolo/yolov8x_person_face.pt") + args.add_argument("--with-persons", action="store_true", default=False, help="If set model will run with persons, if available") + args.add_argument("--disable-faces", action="store_true", default=False, help="If set model will use only persons if available") + args.add_argument("--draw", action="store_true", default=False, help="If set, resulted images will be drawn") + args = args.parse_args([]) + # Initialize + self.predictor_age = Predictor(args) + + # Initialize + self.nude_detector = NudeDetector(model_path="models/nude_detector/640m.onnx", inference_resolution=640) + # detector = NudeDetector(model_path="downloaded_640m.onnx path", inference_resolution=640) + # https://github.com/notAI-tech/NudeNet?tab=readme-ov-file#available-models + + # All labels list + self.nudity_all_labels = [ + "FEMALE_GENITALIA_COVERED", + "FACE_FEMALE", + "BUTTOCKS_EXPOSED", + "FEMALE_BREAST_EXPOSED", + "FEMALE_GENITALIA_EXPOSED", + "MALE_BREAST_EXPOSED", + "ANUS_EXPOSED", + "FEET_EXPOSED", + "BELLY_COVERED", + "FEET_COVERED", + "ARMPITS_COVERED", + "ARMPITS_EXPOSED", + "FACE_MALE", + "BELLY_EXPOSED", + "MALE_GENITALIA_EXPOSED", + "ANUS_COVERED", + "FEMALE_BREAST_COVERED", + "BUTTOCKS_COVERED", + ] + # Classes of interest + self.nudity_classes_of_interest = ["BUTTOCKS_EXPOSED", "FEMALE_BREAST_EXPOSED", "FEMALE_GENITALIA_EXPOSED", "ANUS_EXPOSED", "MALE_GENITALIA_EXPOSED"] + + def _censor(self, image_bgr, detections): + # Copy original image + image_bgr_censored = image_bgr.copy() + + for detection in detections: + box = detection["box"] + x, y, w, h = box[0], box[1], box[2], box[3] + # Change these pixels to pure black + image_bgr_censored[y : y + h, x : x + w] = (0, 0, 0) + + return image_bgr_censored + + def process_image(self, image_bgr): + ################################################################### + # Predict + detected_objects, out_img = self.predictor_age.recognize(image_bgr) + logging.debug("#persons: {}, #faces: {}".format(detected_objects.n_persons, detected_objects.n_faces)) + + # Num faces and persons detected + detected_objects.n_faces, detected_objects.n_persons + # Association + detected_objects.associate_faces_with_persons() + + # detected_objects.face_to_person_map + # {2: 1, 3: 0} + # detected_objects.ages + # [None, None, 27.18, 23.77] + age_predictions = [e for e in detected_objects.ages if e is not None] + + # Crops of faces & persons + # crops = detected_objects.collect_crops(img) + any_minor_present = any([ a < 18 for a in detected_objects.ages if a is not None ]) + ################################################################### + + ################################################################### + # Predict + nude_detections = self.nude_detector.detect(image_bgr) + logging.debug("Nude detections: {}".format(nude_detections)) + # Filter by classes of interest + nude_detections = [ detection for detection in nude_detections if detection["class"] in self.nudity_classes_of_interest ] + # Nude detections present? + any_nude_detection = len(nude_detections) > 0 + ################################################################### + + ################################################################### + # Censor image + censored_img_bgr = self._censor(image_bgr, nude_detections) + # Plot age predictions on censored image + output_image = detected_objects.plot(img=censored_img_bgr) + ################################################################### + + results = { + "any_minor_present": any_minor_present, + "any_nude_detection": any_nude_detection, + "nudity_detections": nude_detections, + "age_predictions": age_predictions, + "image": output_image, + } + return results + +def process(image_bytes): + try: + logging.info("Processing image") + # Convert bytes to NumPy array + img_array = np.frombuffer(image_bytes.getvalue(), dtype=np.uint8) + # Decode image using OpenCV + img_bgr = cv2.imdecode(img_array, cv2.IMREAD_COLOR) + + if img_bgr is None: + return {} + # Process + results = CV().process_image(img_bgr) + logging.info("Returning results") + return results + + except Exception as e: + logging.warning("Error processing image: {}".format(str(e))) + return {} \ No newline at end of file diff --git a/app_cv/imgs/img_1p.jpg b/app_cv/imgs/img_1p.jpg new file mode 100644 index 0000000000000000000000000000000000000000..68781a3d88ffa4ec294a0fb8dbda81b0ded79a2b GIT binary patch literal 36154 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 bJS}c^{{Ume{_OAn0L4*TS#~gY?tlN;)SJ)h literal 0 HcmV?d00001 diff --git a/utils/Ghost-Posts.ipynb b/utils/Ghost-Posts.ipynb index 5f5a883..5e541f9 100644 --- a/utils/Ghost-Posts.ipynb +++ b/utils/Ghost-Posts.ipynb @@ -10,6 +10,7 @@ "import time\n", "import jwt\n", "import requests\n", + "from datetime import datetime, timedelta, timezone\n", "\n", "admin_api_url = \"\"\n", "admin_api_key = \"\"\n", @@ -25,7 +26,15 @@ " 'aud': '/v5/admin/' # Adjust depending on your Ghost version\n", " }\n", " token = jwt.encode(payload, bytes.fromhex(secret), algorithm='HS256', headers=header)\n", - " return token\n" + " return token\n", + "\n", + "# Get token\n", + "jwt_token = _create_jwt(os.getenv(\"GHOST_ADMIN_API_KEY\"))\n", + "\n", + "headers = {\n", + " 'Authorization': f'Ghost {jwt_token}',\n", + " 'Content-Type': 'application/json'\n", + "}" ] }, { @@ -34,33 +43,100 @@ "metadata": {}, "outputs": [], "source": [ - "# Get token\n", - "jwt_token = _create_jwt(os.getenv(\"GHOST_ADMIN_API_KEY\"))\n", + "DELETE_ALL_POSTS = False\n", "\n", - "headers = {\n", - " 'Authorization': f'Ghost {jwt_token}',\n", - " 'Content-Type': 'application/json'\n", - "}\n", + "if DELETE_ALL_POSTS:\n", + " while (True):\n", + " # GET /admin/posts/\n", + " response = requests.get(os.path.join(admin_api_url, \"posts\"), headers=headers)\n", + " dict_response = response.json()\n", "\n", - "deleted_post = True\n", + " if (len(dict_response.get(\"posts\")) == 0):\n", + " break\n", "\n", - "while (deleted_post):\n", - " # GET /admin/posts/\n", - " response = requests.get(os.path.join(admin_api_url, \"posts\"), headers=headers)\n", - " dict_response = response.json()\n", + " # Iterate posts\n", + " for p in dict_response.get(\"posts\"):\n", + " # Post ID\n", + " post_id = p.get(\"id\")\n", "\n", - " if (len(dict_response.get(\"posts\")) == 0):\n", - " deleted_post = False\n", - " break\n", + " # DELETE /admin/posts/{id}/\n", + " r = requests.delete(os.path.join(admin_api_url, \"posts\", \"{}\".format(post_id)), headers=headers)\n", + " print(\"Post:\", post_id, \"Status:\", r.status_code, r.text)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "PUBLISH_SAMPLE = False\n", "\n", - " # Iterate posts\n", - " for p in dict_response.get(\"posts\"):\n", - " # Post ID\n", - " post_id = p.get(\"id\")\n", + "def _create_ghost_post(jwt_token, admin_api_url, post_data):\n", + " # Get Admin API URL\n", + " admin_api_url = os.getenv(\"GHOST_ADMIN_API_URL\")\n", "\n", - " # DELETE /admin/posts/{id}/\n", - " r = requests.delete(os.path.join(admin_api_url, \"posts\", \"{}\".format(post_id)), headers=headers)\n", - " print(\"Post:\", post_id, \"Status:\", r.status_code, r.text)" + " headers = {\n", + " 'Authorization': f'Ghost {jwt_token}',\n", + " 'Content-Type': 'application/json'\n", + " }\n", + " \n", + " post_data = {\"posts\": [post_data]}\n", + "\n", + " response = requests.post(\n", + " os.path.join(admin_api_url, \"posts\"),\n", + " json=post_data,\n", + " headers=headers,\n", + " params={\"source\":\"html\"}\n", + " )\n", + "\n", + " if response.status_code == 201:\n", + " print(\"Ghost post published successfully\")\n", + " return response.json()\n", + " else:\n", + " print(\"Ghost - Failed to publish post: {} {}\".format(response.status_code, response.text))\n", + " return None\n", + "\n", + "if (PUBLISH_SAMPLE):\n", + " url_id = 150\n", + "\n", + " post_data = {\n", + " # \"slug\": \"hey-short\",\n", + " \"title\": \"Hey there, sample title\",\n", + " \"html\": \"

Hey there!

\",\n", + " # \"feature_image\": photo_url,\n", + " # \"feature_image_caption\": \"\",\n", + " \"status\": \"published\",\n", + " \"tags\": [\"#url-id-{}\".format(url_id)]\n", + " }\n", + "\n", + " # Publish post\n", + " payload = _create_ghost_post(jwt_token, admin_api_url, post_data)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Filter by post title\n", + "post_title = \"Funds raised for legal action over failure to stop grooming gangs\"\n", + "# Filter by published date\n", + "iso_time = (datetime.now(timezone.utc) - timedelta(hours=48)).strftime('%Y-%m-%dT%H:%M:%S') + 'Z'\n", + "# Parameter for filter\n", + "params = {\"filter\": \"title:'{}'+published_at:>{}\".format(post_title, iso_time)}\n", + "\n", + "# Filter by URL ID\n", + "url_id = 150\n", + "# Parameter for filter\n", + "params = {\"filter\": \"tags:hash-url-id-{}\".format(url_id)}\n", + "\n", + "# Get posts using filter\n", + "response = requests.get(os.path.join(admin_api_url, \"posts\"), params=params, headers=headers)\n", + "dict_response = response.json()\n", + "\n", + "len(dict_response.get(\"posts\"))" ] } ], diff --git a/utils/Schools-NL.ipynb b/utils/Schools-NL.ipynb index 8f3dd75..823fbdb 100644 --- a/utils/Schools-NL.ipynb +++ b/utils/Schools-NL.ipynb @@ -12,6 +12,8 @@ "import pandas as pd\n", "import os\n", "import json\n", + "import csv\n", + "\n", "\n", "headers = {\"User-Agent\": \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36\"}" ] @@ -69,6 +71,154 @@ " # websites.append(href)\n", " return href\n", "\n", + "def get_num_students_per_zipcode(soup):\n", + " list_zipcode_students_percentage = []\n", + "\n", + " h3_tag = soup.find(\"h3\", string=\"In welk postcodegebied wonen de leerlingen van deze school?\")\n", + " if h3_tag:\n", + " dialog = h3_tag.find_parent(\"dialog\")\n", + "\n", + " if dialog:\n", + " # print(dialog.prettify())\n", + " table = dialog.find(\"table\")\n", + " if table:\n", + " rows = table.find_all(\"tr\")\n", + " for row in rows:\n", + " cells = row.find_all([\"th\", \"td\"])\n", + " row_data = [cell.get_text(strip=True) for cell in cells]\n", + " zipcode, num_students, percentage = row_data\n", + " list_zipcode_students_percentage.append( (zipcode, num_students, percentage) )\n", + " \n", + " return list_zipcode_students_percentage\n", + "\n", + "def get_num_students_trend(soup):\n", + " # Step 1: Locate the tag\n", + " trend_chart_tag = soup.find(\"aantal-leerlingen-trend-line-chart\")\n", + "\n", + " if trend_chart_tag:\n", + " # Step 2: Extract the 'leerlingen-trend-data' attribute\n", + " trend_data_attr = trend_chart_tag.get(\"leerlingen-trend-data\")\n", + " \n", + " if trend_data_attr:\n", + " # Step 3: Parse the JSON string into a Python object\n", + " trend_data = json.loads(trend_data_attr)\n", + " #print(\"Extracted leerlingen-trend-data:\")\n", + " #print(json.dumps(trend_data, indent=4)) # Pretty-print the JSON data\n", + " return [ (e.get(\"key\"), e.get(\"aantal\") ) for e in trend_data]\n", + "\n", + "def get_num_students_per_age_and_group(soup):\n", + " num_students_per_group, num_students_per_age = [], []\n", + " ############################################################################\n", + " # Step 1: Locate the tag\n", + " chart_tag = soup.find('aantal-leerlingen-leeftijd-bar-chart', attrs={'aantal-per-leeftijd': True})\n", + " # Step 2: Extract the 'aantal-per-leeftijd' attribute\n", + " raw_data = chart_tag['aantal-per-leeftijd']\n", + "\n", + " # Step 3: Parse the JSON data\n", + " try:\n", + " data = json.loads(raw_data)\n", + " # Step 4: Print the extracted data\n", + " # print(\"Aantal per Leeftijd:\")\n", + " for entry in data:\n", + " age = entry['key']\n", + " num_students = entry['aantal']\n", + " # school_data[\"num_students_age_{}\".format(age)] = num_students\n", + " num_students_per_age.append( (age, num_students) )\n", + " # print(f\"Age {entry['key']}: {entry['aantal']} leerlingen\")\n", + " except json.JSONDecodeError as e:\n", + " print(f\"Failed to parse JSON data: {e}\")\n", + "\n", + " ############################################################################\n", + " # Step 1: Locate the tag\n", + " chart_tag = soup.find('aantal-leerlingen-leerjaar-bar-chart', attrs={'aantal-per-leerjaar': True})\n", + "\n", + " if not chart_tag:\n", + " print(\"Could not find the 'aantal per leerjaar' section.\")\n", + " else:\n", + " # Step 2: Extract the 'aantal-per-leerjaar' attribute\n", + " raw_data = chart_tag['aantal-per-leerjaar']\n", + " \n", + " # Step 3: Parse the JSON data\n", + " try:\n", + " data = json.loads(raw_data)\n", + " # Step 4: Print the extracted data\n", + " # print(\"Aantal per Leerjaar:\")\n", + " for entry in data:\n", + " group = entry['key']\n", + " num_students = entry['aantal']\n", + " # school_data[\"num_students_group_{}\".format(group)] = num_students\n", + " num_students_per_group.append( (group, num_students) )\n", + " # print(f\"Groep {entry['key']}: {entry['aantal']} leerlingen\")\n", + " except json.JSONDecodeError as e:\n", + " print(f\"Failed to parse JSON data: {e}\")\n", + " ############################################################################\n", + " return num_students_per_group, num_students_per_age\n", + "\n", + "\n", + "def update_school_data(school_url, school_data):\n", + " try:\n", + " # Process school (request contact details)\n", + " response = requests.get(os.path.join(school_url, \"contact/#inhoud\"), headers=headers)\n", + " response.raise_for_status() # Raise an exception for HTTP errors\n", + " # Parse the HTML content using BeautifulSoup\n", + " soup_school = BeautifulSoup(response.text, 'html.parser')\n", + "\n", + " # School details\n", + " school_details = soup_school.find(class_=\"school-details\")\n", + " for category_idx, li_detail in enumerate(school_details.find_all(\"li\")):\n", + " data = li_detail.find('span', class_='infotip-term')['data-dfn']\n", + " text = li_detail.get_text(strip=True)\n", + " # Set data\n", + " school_data[\"category_{}\".format(category_idx)] = text\n", + " school_data[\"category_{}_description\".format(category_idx)] = data\n", + " \n", + " school_address = soup_school.find(class_=\"school-adres\").get_text(strip=True)\n", + " school_postcode_city = soup_school.find(class_=\"school-postcode-woonplaats\").get_text(strip=True)\n", + " school_postcode = \"\".join(school_postcode_city.split(\" \")[:2])\n", + " school_city = \" \".join(school_postcode_city.split(\" \")[2:])\n", + "\n", + " school_data[\"city\"] = school_city\n", + " school_data[\"postcode\"] = school_postcode\n", + " school_data[\"address\"] = school_address\n", + "\n", + " try:\n", + " school_data[\"website\"] = find_website(soup_school) # soup_school.find(class_=\"button button-primary\").get('href')\n", + " except Exception as e:\n", + " pass\n", + " try:\n", + " school_data[\"phone\"] = soup_school.find('a', href=lambda href: href and href.startswith('tel:')).text\n", + " except Exception as e:\n", + " pass\n", + " try:\n", + " school_data[\"email\"] = extract_emails(soup_school)\n", + " except Exception as e:\n", + " pass\n", + "\n", + " # Process school main site\n", + " response = requests.get(os.path.join(school_url), headers=headers)\n", + " response.raise_for_status() # Raise an exception for HTTP errors\n", + " # Parse the HTML content using BeautifulSoup\n", + " soup_school = BeautifulSoup(response.text, 'html.parser')\n", + "\n", + " try:\n", + " school_data[\"students_per_zipcode\"] = get_num_students_per_zipcode(soup_school)\n", + " except Exception as e:\n", + " pass\n", + " try:\n", + " school_data[\"students_per_year_trend\"] = get_num_students_trend(soup_school)\n", + " except Exception as e:\n", + " pass\n", + "\n", + " if (school_data.get(\"category\").lower() == \"basisscholen\"):\n", + " try:\n", + " num_students_per_group, num_students_per_age = get_num_students_per_age_and_group(soup_school)\n", + " school_data[\"num_students_per_group\"] = num_students_per_group if len(num_students_per_group)>0 else None\n", + " school_data[\"num_students_per_age\"] = num_students_per_age if len(num_students_per_age)>0 else None\n", + " except Exception as e:\n", + " pass\n", + " \n", + " except Exception as e:\n", + " print(school_url, str(e))\n", "\n", "def main():\n", " list_urls = [\n", @@ -129,100 +279,26 @@ " \"url\": school_url,\n", " }\n", "\n", - " try:\n", - " # Process school (request contact details)\n", - " response = requests.get(os.path.join(school_url, \"contact/#inhoud\"), headers=headers)\n", - " response.raise_for_status() # Raise an exception for HTTP errors\n", - "\n", - " # Parse the HTML content using BeautifulSoup\n", - " soup_school = BeautifulSoup(response.text, 'html.parser')\n", - "\n", - " # School details\n", - " school_details = soup_school.find(class_=\"school-details\")\n", - " for category_idx, li_detail in enumerate(school_details.find_all(\"li\")):\n", - " data = li_detail.find('span', class_='infotip-term')['data-dfn']\n", - " text = li_detail.get_text(strip=True)\n", - " # Set data\n", - " school_data[\"category_{}\".format(category_idx)] = text\n", - " school_data[\"category_{}_description\".format(category_idx)] = data\n", - " \n", - " school_address = soup_school.find(class_=\"school-adres\").get_text(strip=True)\n", - " school_postcode_city = soup_school.find(class_=\"school-postcode-woonplaats\").get_text(strip=True)\n", - " school_postcode = \"\".join(school_postcode_city.split(\" \")[:2])\n", - " school_city = \" \".join(school_postcode_city.split(\" \")[2:])\n", - "\n", - " school_data[\"city\"] = school_city\n", - " school_data[\"postcode\"] = school_postcode\n", - " school_data[\"address\"] = school_address\n", - "\n", - " try:\n", - " school_data[\"website\"] = find_website(soup_school) # soup_school.find(class_=\"button button-primary\").get('href')\n", - " except Exception as e:\n", - " pass\n", - " try:\n", - " school_data[\"phone\"] = soup_school.find('a', href=lambda href: href and href.startswith('tel:')).text\n", - " except Exception as e:\n", - " pass\n", - " try:\n", - " school_data[\"email\"] = extract_emails(soup_school)\n", - " except Exception as e:\n", - " pass\n", - "\n", - " if (category.lower() == \"basisscholen\"):\n", - " ############################################################################\n", - " # Step 1: Locate the tag\n", - " chart_tag = soup.find('aantal-leerlingen-leeftijd-bar-chart', attrs={'aantal-per-leeftijd': True})\n", - " # Step 2: Extract the 'aantal-per-leeftijd' attribute\n", - " raw_data = chart_tag['aantal-per-leeftijd']\n", - "\n", - " # Step 3: Parse the JSON data\n", - " try:\n", - " data = json.loads(raw_data)\n", - " # Step 4: Print the extracted data\n", - " print(\"Aantal per Leeftijd:\")\n", - " for entry in data:\n", - " age = entry['key']\n", - " num_students = entry['aantal']\n", - " school_data[\"num_students_age_{}\".format(age)] = num_students\n", - " # print(f\"Age {entry['key']}: {entry['aantal']} leerlingen\")\n", - " except json.JSONDecodeError as e:\n", - " print(f\"Failed to parse JSON data: {e}\")\n", - "\n", - " # Step 1: Locate the tag\n", - " chart_tag = soup.find('aantal-leerlingen-leerjaar-bar-chart', attrs={'aantal-per-leerjaar': True})\n", - "\n", - " ############################################################################\n", - " # Step 1: Locate the tag\n", - " chart_tag = soup.find('aantal-leerlingen-leerjaar-bar-chart', attrs={'aantal-per-leerjaar': True})\n", - "\n", - " if not chart_tag:\n", - " print(\"Could not find the 'aantal per leerjaar' section.\")\n", - " else:\n", - " # Step 2: Extract the 'aantal-per-leerjaar' attribute\n", - " raw_data = chart_tag['aantal-per-leerjaar']\n", - " \n", - " # Step 3: Parse the JSON data\n", - " try:\n", - " data = json.loads(raw_data)\n", - " # Step 4: Print the extracted data\n", - " print(\"Aantal per Leerjaar:\")\n", - " for entry in data:\n", - " group = entry['key']\n", - " num_students = entry['aantal']\n", - " school_data[\"num_students_group_{}\".format(group)] = num_students\n", - " print(f\"Groep {entry['key']}: {entry['aantal']} leerlingen\")\n", - " except json.JSONDecodeError as e:\n", - " print(f\"Failed to parse JSON data: {e}\")\n", - " ############################################################################\n", - " except Exception as e:\n", - " print(school_url, str(e))\n", - " # assert False\n", + " update_school_data(school_url, school_data)\n", "\n", " list_school_data_dicts.append(school_data)\n", "\n", - " df = pd.DataFrame(list_school_data_dicts)\n", - " df.to_csv(\"scholenopdekaart.csv\")\n", + " # Save per processed school to track progress\n", + " df = pd.DataFrame(list_school_data_dicts)\n", + " df.to_csv(\"scholenopdekaart_tmp.csv\", encoding=\"utf-8\", quoting=csv.QUOTE_ALL)\n", "\n", + " df = pd.DataFrame(list_school_data_dicts)\n", + " df.to_csv(\"scholenopdekaart.csv\", encoding=\"utf-8\", quoting=csv.QUOTE_ALL)\n", + " # Without extra columns\n", + " df.drop(columns=[\"students_per_zipcode\", \"students_per_year_trend\", \"num_students_per_group\", \"num_students_per_age\"]).to_csv(\"scholenopdekaart_.csv\", encoding=\"utf-8\", quoting=csv.QUOTE_ALL)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ "\"\"\" # Issues with URL:\n", "https://scholenopdekaart.nl/middelbare-scholen/grave/1900/merletcollege-grave/\n", "https://scholenopdekaart.nl/middelbare-scholen/lent/4386/citadel-college-locatie-griftdijk/\n", @@ -259,24 +335,8 @@ "metadata": {}, "outputs": [], "source": [ - "'''\n", - "school_url = \"https://scholenopdekaart.nl/basisscholen/aalden/9661/christelijke-basisschool-de-schutse/\"\n", - "response = requests.get(os.path.join(school_url, \"contact/#inhoud\"), headers=headers)\n", - "# Parse the HTML content using BeautifulSoup\n", - "soup_school = BeautifulSoup(response.text, 'html.parser')\n", - "soup_school\n", - "'''" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import pandas as pd\n", - "\n", "df = pd.read_csv(\"scholenopdekaart.csv\", index_col=0)\n", + "\n", "df.head()" ] }, @@ -288,122 +348,6 @@ "source": [ "df.tail()" ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# https://scholenopdekaart.nl/middelbare-scholen/hardenberg/26614/de-ambelt/\n", - "# From which zip codes the students come\n", - "# How many kids passed the exams', ...\n", - "\n", - "import requests\n", - "from bs4 import BeautifulSoup\n", - "import os\n", - "\n", - "url = \"https://scholenopdekaart.nl/middelbare-scholen/hardenberg/26614/de-ambelt/\"\n", - "response = requests.get( os.path.join(url, \"#inhoud\") )\n", - "\n", - "\n", - "url = \"https://scholenopdekaart.nl/basisscholen/aalden/9661/christelijke-basisschool-de-schutse/\"\n", - "response = requests.get(url)\n", - "\n", - "soup = BeautifulSoup(response.content, 'html.parser')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "\n", - "# Locate the section containing \"Welke profielen volgen de examendeelnemers?\"\n", - "section_header = soup.find('h3', string=\"Welke profielen volgen de examendeelnemers?\")\n", - "if not section_header:\n", - " raise ValueError(\"Section 'Welke profielen volgen de examendeelnemers?' not found in the HTML.\")\n", - "\n", - "# Navigate to the parent section or subsection\n", - "section = section_header.find_parent('section')\n", - "if not section:\n", - " raise ValueError(\"Parent section for 'Welke profielen volgen de examendeelnemers?' not found.\")\n", - "\n", - "# Check if the section contains a message indicating no data is available\n", - "no_data_message = section.find('p', string=\"Deze informatie is voor deze school niet bekend.\")\n", - "if no_data_message:\n", - " print(\"No data available for 'Welke profielen volgen de examendeelnemers?'.\")\n", - "else:\n", - " # Extract the relevant content (e.g., tables, lists, or paragraphs)\n", - " content = []\n", - " for element in section.find_all(['p', 'table', 'ul', 'ol']):\n", - " if element.name == 'table':\n", - " # Extract table rows\n", - " rows = element.find_all('tr')\n", - " for row in rows:\n", - " cells = row.find_all(['th', 'td'])\n", - " row_data = [cell.get_text(strip=True) for cell in cells]\n", - " content.append(row_data)\n", - " else:\n", - " # Extract text from paragraphs, lists, etc.\n", - " content.append(element.get_text(strip=True))\n", - "\n", - " # Print the extracted content\n", - " for item in content:\n", - " print(item)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "\n", - "# Locate the dialog containing the table\n", - "dialog = soup.find('dialog', class_='modal modal-dialog')\n", - "if not dialog:\n", - " raise ValueError(\"Dialog element not found in the HTML.\")\n", - "\n", - "# Locate the table within the dialog\n", - "table = dialog.find('table')\n", - "if not table:\n", - " raise ValueError(\"Table element not found within the dialog.\")\n", - "\n", - "# Extract table headers\n", - "headers = [header.get_text(strip=True) for header in table.find_all('th')]\n", - "\n", - "# Extract table rows\n", - "data = []\n", - "for row in table.find_all('tr')[1:]: # Skip the header row\n", - " cells = row.find_all('td')\n", - " if len(cells) == len(headers): # Ensure the row matches the expected structure\n", - " row_data = {\n", - " headers[0]: cells[0].get_text(strip=True), # Postcodegebied\n", - " headers[1]: cells[1].get_text(strip=True), # Aantal leerlingen\n", - " headers[2]: cells[2].get_text(strip=True) # Percentage\n", - " }\n", - " data.append(row_data)\n", - "\n", - "# Print the extracted data\n", - "for entry in data:\n", - " print(entry)" - ] } ], "metadata": { diff --git a/utils/Summary.ipynb b/utils/Summary.ipynb new file mode 100644 index 0000000..3fe844f --- /dev/null +++ b/utils/Summary.ipynb @@ -0,0 +1,182 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# docker exec -it ollama_npu bash\n", + "# rkllama pull\n", + "#\n", + "# c01zaut/Llama-3.2-3B-Instruct-rk3588-1.1.4\n", + "# Llama-3.2-3B-Instruct-rk3588-w8a8-opt-0-hybrid-ratio-0.0.rkllm\n", + "# Llama-3.2-3B-Instruct-rk3588-w8a8-opt-0-hybrid-ratio-0.5.rkllm\n", + "# Llama-3.2-3B-Instruct-rk3588-w8a8-opt-1-hybrid-ratio-0.0.rkllm\n", + "# Llama-3.2-3B-Instruct-rk3588-w8a8-opt-1-hybrid-ratio-0.5.rkllm\n", + "# Llama-3.2-3B-Instruct-rk3588-w8a8_g512-opt-1-hybrid-ratio-0.5.rkllm\n", + "#\n", + "# c01zaut/Qwen2.5-3B-Instruct-RK3588-1.1.4\n", + "# Qwen2.5-3B-Instruct-rk3588-w8a8-opt-1-hybrid-ratio-0.0.rkllm\n", + "# Qwen2.5-3B-Instruct-rk3588-w8a8-opt-1-hybrid-ratio-1.0.rkllm\n", + "# Qwen2.5-3B-Instruct-rk3588-w8a8_g256-opt-1-hybrid-ratio-1.0.rkllm\n", + "#" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [], + "source": [ + "import ollama\n", + "import os\n", + "import requests\n", + "import json\n", + "from pprint import pprint\n", + "\n", + "# endpoint = \"https://ollamamodelnpu.matitos.org\"\n", + "endpoint = \"https://ollamamodel.matitos.org\"\n", + "model = \"qwen3:0.6b\"\n", + "model = \"qwen3:1.7b\"\n", + "client = ollama.Client(endpoint)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "r = requests.post( os.path.join(endpoint, \"unload_model\") )\n", + "r.status_code, r.json()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "r = requests.get( os.path.join(endpoint, \"models\") )\n", + "r.json().get(\"models\"), [ m.model for m in client.list().get(\"models\") ]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "model = \"llama3-instruct:3b\"\n", + "model = \"qwen2.5-instruct:3b\"" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "article_content = \"Kevin Sutherland's message to Rowan Lumsden told of his agony at what he believed were malicious rumours about his life. The best friend of tragic Kevin Sutherland has revealed a heartbreaking message sent in the last hours of his life. Rowan Lumsden, 35, says Kevin’s death would have been avoided if his request for anonymity in the Scottish Child Abuse Inquiry had been accepted. Mum-of-one Rowan told how her friend sent a 17-minute voice message that culminated as he stood on the Forth Road Bridge, where he is thought to have plunged to his death on December 19. The Daily Record has told how Kevin, 33, had ticked a box to say he approved of his testimony of historic abuse that he suffered to be published online. Kevin’s family later revealed an email sent to the inquiry, in which he begged for his real name to be redacted, suggesting he may take his own life if he was not given that protection. His appeal was dismissed by SCAI chair Lady Smith. Rowan told how Kevin left a harrowing final message, telling of his agony at what he believed to be malicious rumours that plagued his life. Rowan said: “I was asleep when the messages came in and it was devastating to hear his voice, knowing where he was and what was going to happen. I just wish I could have helped. “Kevin was pushed to the limit and he was so troubled about what people were saying about him. “He lived in fear his testimony would be used by people to make him out to be at fault or misconstrued and he bitterly regretted his decision to allow it to be made public. “I have no doubt that he would be alive today if he was allowed to to retract his on story from the record.” Rowan, 35, said Lady Smith’s decision was wrong “in so many ways”. She said: “He begged her to let him be anonymous and he said that he would take his life if she refused. “But she said, ‘No’. I cannot see any way that can be explained away. He just needed the time it took to get the right interventions to turn his mental health and his life around. “Lady Smith was the top person in the inquiry. She knew she was dealing with a hugely vulnerable person – as all victims are. She knew that he was having suicidal thoughts.” Kevin suffered trauma, including sexual abuse, in his childhood. In his final message to Rowan, in the hours before his suspected death, Kevin didn’t refer directly to the SCAI inquiry but stated: “It’s just coming from the ­absolute f****** heart and I just cannot cope with this life any more. “It’s just been so f****** unbelievably brutal. I kind of feel like, what’s the point? People have got their preconceived ideas and malicious gossip has served such a toxic contribution to this final decision that I’ve made. “That’s me on the bridge. End of the road, eh? End of the road to all the liars and doubters and gossip mongrels.” Kevin’s sister Melanie Watson, who recently revealed the text of Kevin’s final appeal for anonymity, said she was aware of his final messages to friends. She added: “He was very fixated with the fear that people would make false assumptions about him, based on reading his testimony on Google.” The inquiry’s handling of Kevin is now part of an independent inquiry. An SCAI spokesperson said: “SCAI has commissioned an independent review to consider all aspects of its interactions with Kevin.”\"\n", + "article_content = \"Child services visited a Bronx apartment while a 4-year-old girl was trapped inside with the corpses of her troubled mom and brother – but walked away after knocking, neighbors said. Lisa Cotton, 38, and her 8-year-old son, Nazir Millien, 8, had been dead for at least two weeks before relatives found them and the toddler inside the house of horrors Friday, one day after reps for the Administration for Children’s Services dropped the ball, neighbor Sabrina Coleson said. “They didn’t do s–t,” Coleson said Sunday. “They were here ringing people’s bells the day before the wellness check. They were here, but they didn’t do s–t. “One rang my bell and asked if I had any concerns for upstairs. And then a man opened his door and started yelling,” she said. “Lisa was a very cool girl. I never saw her son with her, only the girl. It’s terrible.” Concerned relatives finally checked on the family on Friday and found the 4-year-old, Promise, alone, starving and in horrid condition on her mother’s bed — as bugs crawled over her dead family. Cotton’s father, Hubert, 71, had sent his oldest granddaughter to check the apartment at East 231st Street — with the woman grabbing her young sibling and fleeing the putrid home to call police. ACS wasn’t the only city agency to leave Promise trapped in hellish conditions — neighbors said cops were also called to the apartment on Tuesday but left after not sensing the stench reported by others. Hubert Cotton said the toddler survived by “feeding herself with chocolate.” Law enforcement sources said Lisa Cotton had a history of erratic behavior, and had a pending ACS case for alleged child neglect before she was found dead. She was arrested in 2021 on child abandonment charges after police said she was caught swinging her then-infant daughter around in a stroller and lighting a wig on fire on White Plains Road, sources said. When cops arrived she was allegedly walking away, leaving Promise behind. The outcome of the case was not available because the file is sealed. One neighbor said the mom had “episodes” in the past. Sources said police believe Lisa Cotton, who suffered from asthma, may have died from cardiac arrest, while her son, who was born prematurely and had a feeding tube, may have starved to death. A spokesperson for ACS declined to comment on the case on Sunday other than to say the agency is “investigating this tragedy with the NYPD.”\"\n", + "\n", + "# prompt = \"Rewrite the content below into a clear and concise summary of one paragraph maximum, presenting the key points as if they are newly written insights. Do not mention or reference the original text, its source, or any phrases like 'According to' or 'The text states'. Write in a natural, standalone format that feels like an original explanation. Keep it brief, engaging, informative, in the style of a news article:\\n\\n{}\".format(article_content)\n", + "# prompt = \"Provide a summary of the content below, presenting the key points as if they are newly written insights. Write in a natural, standalone format that feels like an original explanation. Do not mention or reference the original text, its source, or any phrases like 'According to' or 'The text states'. Keep it brief, engaging, informative, in the style of a news article, and in one single paragraph:\\n\\n{}\".format(article_content)\n", + "# prompt = \"Provide a summary of the content below, writing in a natural and standalone format that feels like an original explanation. Do not mention or reference the original text, its source, or any phrases like 'According to' or 'The text states'. Keep it brief, engaging, informative, in the style of a news article, and in one single paragraph:\\n\\n{}\".format(article_content)\n", + "\n", + "# in one sentence each\n", + "prompt = \"First, provide a summary of the content below in one paragraph. Second, specify the Who, What, When, Where and Why of the story:\\n\\n{}\".format(article_content)\n", + "# prompt = \"Provide the 5W (Who, What, When, Where, Why) and a detailed summary of the content below:\\n\\n{}\".format(article_content)\n", + "# Only answer with the location or address which can be extracted from this description\n", + "\n", + "prompt = \"Provide, in one sentence each, the who, what, when, where, why, and a detailed summary of the content below:\\n\\n{}\".format(article_content)" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{}\n" + ] + } + ], + "source": [ + "options = {\"temperature\": 0, \"seed\": 51029}\n", + "resp = client.generate(model=model, prompt=prompt, format=\"json\", options=options)\n", + "r = requests.post( os.path.join(endpoint, \"unload_model\") )\n", + "\n", + "response_dict = json.loads(resp.response)\n", + "pprint(response_dict)" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'{\\n\\n\\n}'" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "resp.response" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"\\nOkay, let's tackle this query. The user wants a one-sentence summary for each element: who, what, when, where, why, and a detailed summary.\\n\\nFirst, the main event is the child services visiting a Bronx apartment with a 4-year-old trapped, but the neighbors say they knocked out the corpses. So for the first sentence, I need to include who (child services), what (visited the apartment), when (Friday), where (the apartment), why (neighbors said they didn't do it), and a summary. \\n\\nThen, for the second part, the user might want more details. Let me check the content. The summary needs to include the specific details like the family members, the days they were found dead, the agencies involved, and the outcomes. Also, mention the sources like ACS and the neighbors' statements. I need to make sure each sentence is concise and covers all the points without being too lengthy. Let me structure each sentence to fit the required format.\\n\\n\\n**Who:** Child services in the Bronx, **What:** Visited an apartment containing a 4-year-old trapped with a dead mom and brother, **When:** Friday, **Where:** East 231st Street, **Why:** Neighbors reported the agency’s actions were inadequate, **Summary:** Child services visited a Bronx apartment with a 4-year-old trapped and dead, neighbors say they knocked out the corpses, and the incident is attributed to the agency’s failure to address the situation, with the family surviving by feeding themselves and the case being sealed.\"" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "#resp = client.generate(model=model, prompt=prompt, format=\"json\")\n", + "resp = client.generate(model=model, prompt=prompt)\n", + "resp.response" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "matitos_urls", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.9" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +}