{
  "source_file": "0930",
  "source_platform": "Hashnode",
  "author": "Wesley Kambale",
  "title_listed": "Luganda Inference on Gemma 3",
  "keywords": "",
  "domain": "大模型算法",
  "content_type": "深度技术解析",
  "is_paid_flag": "否",
  "url": "https://kambale.dev/luganda-inference-on-gemma-3",
  "publish_date": "2025-03-17",
  "corpus_stage": "",
  "notes": "",
  "id": "adb8b16dc257",
  "collected_at": "2026-10-02T14:11:13",
  "title": "Luganda Inference on Gemma 3",
  "body_markdown": "Luganda Inference on Gemma 3\nRunning an inference in Luganda on the Gemma 3 models for image and video tasks\nUpdated\nMarch 17, 2025\n•\n7\nmin read\n•\nView as Markdown\nW\nWesley Kambale\nI'm a Machine Learning Engineer passionate about building production-ready ML systems for the African market.\nWith experience in TensorFlow, Keras, and Python-based workflows, I help teams bridge the gap between machine learning research and real-world deployment—especially on resource-constrained devices.\nI'm also a Google Developer Expert in AI. I regularly speak at tech conferences including PyCon Africa, DevFest Kampala, DevFest Nairobi and more and also write technical articles on AI/ML here.\nPart of series\nMachine Learning\nIntroduction\nGoogle has unveiled\nGemma 3\n, the latest iteration of its open AI models, featuring four versions:\ngemma-3-1b-it\n,\ngemma-3-4b-it\n,\ngemma-3-12b-it\n, and\ngemma-3-27b-it\n.\nThe\ngemma-3-1b-it\nmodel is limited to\ntext-only input\n, supports\nEnglish exclusively\n, and comes with a\n32k context length\n. Due to its lack of multilingual capabilities, it is unsuitable for a\nLuganda inference\n.\nIn contrast, the\ngemma-3-4b-it\n,\ngemma-3-12b-it\n,\nand\ngemma-3-27b-it\nmodels support\nboth text and image input\n, recognize\n140+ languages\n, and offer an extended\n128k context length\n, making them far better suited for multilingual tasks.\nFor this specific task, we are using\ngemma-3-4b-it\ndue to its balance of\nperformance and efficiency\n.\nAccessing Gemma 3 models\nBefore using Gemma 3 for the first time, you must request access to the model through Hugging Face by completing the following steps:\nLog in to\nHugging Face\n, or create a new Hugging Face account if you don't already have one.\nGo to the\nGemma 3 model card\nto get access to the model.\nComplete the consent form and accept the terms and conditions.\nTo generate a Hugging Face token, open your\nSettings\npage in Hugging Face\n, choose\nAccess Tokens\noption in the left pane and click\nNew token\n. In the next window that appears, give a name to your token and choose the type as\nWrite\nto get the write access.\nThen, in Colab, select\nSecrets\n(🔑) in the left pane and add your Hugging Face token. Store your Hugging Face token under the name\nHF_TOKEN\n.\nSelect the runtime\nTo complete this tutorial, you'll need to have a Colab runtime with sufficient resources to load the Gemma 3 model. In this case, a T4/L4 GPU would be needed to load the model weights.\nIn the upper-right of the Colab window, click the dropdown menu.\nSelect\nChange runtime type\n.\nUnder\nHardware accelerator\n, select\nT4 or L4\n.\nInstall Transformers\n!pip install git+https://github.com/huggingface/transformers@v4.49.0-Gemma-3\nImport libraries and dependencies\nfrom\ntransformers\nimport\nAutoProcessor, Gemma3ForConditionalGeneration\nfrom\nPIL\nimport\nImage\nimport\ncv2\nfrom\nIPython.display\nimport\nMarkdown, HTML\nfrom\nbase64\nimport\nb64encode\nimport\nrequests\nimport\ntorch\nChoose the Gemma 3 model variant to use\nGemma 3 is available in four sizes, each supporting different features:\ngemma-3-1b-it\nSupports only text input and English language\n32k context length\ngemma-3-4b-it\n,\ngemma-3-12b-it\n,\ngemma-3-27b-it\nSupports both text and image input\nSupports 140+ languages\n128k context length\nmodel_name =\n'gemma-3-4b-it'\n#We are using 4b\nmodel_id =\nf\"google/\n{model_name}\n\"\nmodel = Gemma3ForConditionalGeneration.from_pretrained(\n    model_id, device_map=\n\"auto\"\n, torch_dtype=torch.bfloat16,\n).eval()\n\nprocessor = AutoProcessor.from_pretrained(model_id)\nDefine helper functions\nresize_image\n: Resizes the input images to\nn x n\npixels, ensuring the aspect ratio is preserved.\nget_model_response\n: Send a text prompt and an image to the model, and retrieve the model's response.\nextract_frames\n: Extracts a specified number of evenly spaced frames from a video file along with their timestamps.\nshow_video\n: Embeds and displays a video in an HTML5 player.\ndef\nresize_image\n(\nimage_path\n):\nimg = Image.open(image_path)\n\n    target_width, target_height =\n640\n,\n640\n# Calculate the target size (maximum width and height).\nif\ntarget_width\nand\ntarget_height:\n        max_size = (target_width, target_height)\nelif\ntarget_width:\n        max_size = (target_width, img.height)\nelif\ntarget_height:\n        max_size = (img.width, target_height)\n\n    img.thumbnail(max_size)\nreturn\nimg\ndef\nget_model_response\n(\nimg: Image, prompt: str, model, processor\n):\n# Prepare the messages for the model.\nmessages = [\n        {\n\"role\"\n:\n\"system\"\n,\n\"content\"\n: [{\n\"type\"\n:\n\"text\"\n,\n\"text\"\n:\n\"You are a helpful assistant. Reply only with the answer to the question asked in Luganda language only, and avoid using additional text in your response like 'here's the answer'.\"\n}]\n        },\n        {\n\"role\"\n:\n\"user\"\n,\n\"content\"\n: [\n                {\n\"type\"\n:\n\"image\"\n,\n\"image\"\n: img},\n                {\n\"type\"\n:\n\"text\"\n,\n\"text\"\n: prompt}\n            ]\n        }\n    ]\n# Tokenize inputs and prepare for the model.\ninputs = processor.apply_chat_template(\n        messages, add_generation_prompt=\nTrue\n, tokenize=\nTrue\n,\n        return_dict=\nTrue\n, return_tensors=\n\"pt\"\n).to(model.device, dtype=torch.bfloat16)\n\n    input_len = inputs[\n\"input_ids\"\n].shape[\n-1\n]\n# Generate response from the model.\nwith\ntorch.inference_mode():\n        generation = model.generate(**inputs, max_new_tokens=\n100\n, do_sample=\nFalse\n)\n        generation = generation[\n0\n][input_len:]\n# Decode the response.\nresponse = processor.decode(generation, skip_special_tokens=\nTrue\n)\nreturn\nresponse\ndef\nextract_frames\n(\nvideo_path, num_frames\n):\n\"\"\"\n    The function is adapted from:\n    https://github.com/merveenoyan/smol-vision/blob/main/Gemma_3_for_Video_Understanding.ipynb\n    \"\"\"\ncap = cv2.VideoCapture(video_path)\nif\nnot\ncap.isOpened():\n        print(\n\"Error: Could not open video file.\"\n)\nreturn\n[]\n\n    total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))\n    fps = cap.get(cv2.CAP_PROP_FPS)\n# Calculate the step size to evenly distribute frames across the video.\nstep = total_frames // num_frames\n    frames = []\nfor\ni\nin\nrange(num_frames):\n        frame_idx = i * step\n        cap.set(cv2.CAP_PROP_POS_FRAMES, frame_idx)\n        ret, frame = cap.read()\nif\nnot\nret:\nbreak\nimg = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))\n        timestamp = round(frame_idx / fps,\n2\n)\n        frames.append((img, timestamp))\n\n    cap.release()\nreturn\nframes\ndef\nshow_video\n(\nvideo_path, video_width =\n600\n):\nvideo_file = open(video_path,\n\"r+b\"\n).read()\n  video_url =\nf\"data:video/mp4;base64,\n{b64encode(video_file).decode()}\n\"\nvideo_html =\nf\"\"\"<video width=\n{video_width}\ncontrols><source src=\"\n{video_url}\n\"></video>\"\"\"\nreturn\nHTML(video_html)\nRun an inference on images\nFetch some sample images for inferencing.\n!wget https://raw.githubusercontent.com/wkambale/Luganda-Inference-on-Gemma-3/main/assets/image_1.jpg -O /content/image_1.jpg\n!wget https://raw.githubusercontent.com/wkambale/Luganda-Inference-on-Gemma-3/main/assets/image_2.jpg -O /content/image_2.jpg\n!wget https://raw.githubusercontent.com/wkambale/Luganda-Inference-on-Gemma-3/main/assets/image_3.jpg -O /content/image_3.jpg\n!wget https://raw.githubusercontent.com/wkambale/Luganda-Inference-on-Gemma-3/main/assets/image_4.png -O /content/image_4.png\nImage 1 Credit: The Pearl\nTask 1: Describe an image\nThe prompt is in Luganda language which translates to: \"Describe the image.\"\nimage_file =\n'image_1.jpg'\nprompt =\n\"Nnyonnyola emmeere eri mu kifaananyi.\"\nimg = resize_image(image_file)\ndisplay(img)\nresponse = get_model_response(img, prompt, model, processor)\ndisplay(Markdown(response))\nResponse:\nOmukoyogo.\nExample 2: Identify a landmark\nThe prompt is in Luganda language which translates to: \"Identify the famous landmark and location\"\nImage 2: Ali Zali\nimage_file =\n'image_2.jpg'\nprompt =\n\"Londoola ekifo kino ekimanyiddwa ennyo nne w'ekisangibwa.\"\nimg = resize_image(image_file)\ndisplay(img)\nresponse = get_model_response(img, prompt, model, processor)\ndisplay(Markdown(response))\nResponse:\nEbibuga.\nImage 3: The Tower Post\nimage_file =\n'image_3.jpg'\nprompt =\n\"Londoola ekifo kino ekimanyiddwa ennyo nne w'ekisangibwa.\"\nimg = resize_image(image_file)\ndisplay(img)\nresponse = get_model_response(img, prompt, model, processor)\ndisplay(Markdown(response))\nResponse:\nKampala Bbwalo.\nTask 3: Mathematical Reasoning/Okulowooza mu Kubala\nThe prompt is in Luganda language which translates to: \"What is the value of x?\"\nImage: Nitin\nfrom\nPIL\nimport\nImage\nfrom\nIPython.display\nimport\nMarkdown\n\nimage_file =\n'image_4.png'\nprompt =\n\"Omuwendo gwa x gwe guliwa?\"\nimg = resize_image(image_file)\ndisplay(img)\nresponse = get_model_response(img, prompt, model, processor)\ndisplay(Markdown(response))\nResponse:\nx = 3\nInference on videos\nThe video is a clip from \"Why Uganda is the Pearl Of Africa!\" shoot.\nCredits: Eunice Tess\nSource:\nYouTube\n# Video file.\nvideo_path =\n\"video.mp4\"\n# No. of frames to be extracted from the video.\nnum_frames =\n10\nvideo_output = show_video(video_path, video_width=\n800\n)\ndisplay(video_output)\nhttps://youtu.be/u4D20WDrZyY?si=uigb9f9hjCFjFxPV\nThe prompt is in Luganda language which translates to: \"Please summarize what is happening in this video\"\nvideo_frames = extract_frames(video_path, num_frames=num_frames)\n\nmessages = [\n    {\n\"role\"\n:\n\"system\"\n,\n\"content\"\n: [{\n\"type\"\n:\n\"text\"\n,\n\"text\"\n:\n\"You are a helpful assistant.\"\n}]\n    },\n    {\n\"role\"\n:\n\"user\"\n,\n\"content\"\n: [{\n\"type\"\n:\n\"text\"\n,\n\"text\"\n:\n\"Nsaba mufunze ebigenda mu maaso mu katambi kano\"\n}]\n    }\n]\n# Add frames to the messages structure.\nfor\nframe_data\nin\nvideo_frames:\n    img, timestamp = frame_data\n    messages[\n1\n][\n\"content\"\n].append({\n\"type\"\n:\n\"text\"\n,\n\"text\"\n:\nf\"Frame at\n{timestamp}\nseconds:\"\n})\n    img.save(\nf\"/content/frames/frame_\n{timestamp}\n.png\"\n)\n    messages[\n1\n][\n\"content\"\n].append({\n\"type\"\n:\n\"image\"\n,\n\"url\"\n:\nf\"/content/frames/frame_\n{timestamp}\n.png\"\n})\n\n\ninputs = processor.apply_chat_template(\n    messages, add_generation_prompt=\nTrue\n, tokenize=\nTrue\n,\n    return_dict=\nTrue\n, return_tensors=\n\"pt\"\n).to(model.device)\n\n\ninput_length = inputs[\n\"input_ids\"\n].shape[\n-1\n]\n# Generate a response based on the inputs.\noutput = model.generate(**inputs, max_new_tokens=\n500\n, do_sample=\nFalse\n)\noutput = output[\n0\n][input_length:]\nresponse = processor.decode(output, skip_special_tokens=\nTrue\n)\n\ndisplay(Markdown(response))\nResponse:\nOkay, let's look at these images. Here's what I see in Luganda:\n\nFrame 0.0: \"Nyo mu maaso, ttiima nnyo. Twee nyo mu nsi y'omutwe, nti nyo mu maaso.\" (It's beautiful, very impressive. It's in a world of wonder, it's in the water.)\nFrame 13.2: \"Nyo mu maaso, ttiima nnyo. Twee nyo mu nsi y'omutwe, nti nyo mu maaso.\" (It's beautiful, very impressive. It's in a world of wonder, it's in the water.)\nFrame 26.4: \"Nyo mu maaso, ttiima nnyo. Twee nyo mu nsi y'omutwe, nti nyo mu maaso.\" (It's beautiful, very impressive. It's in a world of wonder, it's in the water.)\nFrame 39.6: \"Nyo mu maaso, ttiima nnyo. Twee nyo mu nsi y'omutwe, nti nyo mu maaso.\" (It's beautiful, very impressive. It's in a world of wonder, it's in the water.)\nFrame 52.8: \"Nyo mu maaso, ttiima nnyo. Twee nyo mu nsi y'omutwe, nti nyo mu maaso.\" (It's beautiful, very impressive. It's in a world of wonder, it's in the water.)\nFrame 66.0: \"Nyo mu maaso, ttiima nnyo. Twee nyo mu nsi y'omutwe, nti nyo mu maaso.\" (It's beautiful, very impressive. It's in a world of wonder, it's in the water.)\nFrame 79.2: \"Nyo mu maaso, ttiima nnyo. Twee nyo mu nsi y'omutwe, nti nyo mu maaso.\"\nDeductions\nThe outputs above reveal a significant limitation: despite\nGemma 3 models\nboasting multilingual capabilities across\n140+ languages\n, they still struggle to handle\nvision tasks (images and videos) in Luganda\neffectively.\nThis demonstration underscores the urgent need for:\nMore research\ninto optimizing AI models for low-resource languages like Luganda.\nExpanding datasets\nwith high-quality, Luganda-specific image and video annotations.\nTraining foundational models\nthat natively understand Luganda in multimodal contexts.\nWithout these critical steps, AI-powered vision systems will continue to exclude Luganda and other underrepresented languages from advancements in\nmultimodal AI\n.\nResources\nHere is the\nnotebook\n.\n#\nluganda\n#\ngemma\n#\ngenai\n#\ninference",
  "metadata": {
    "title": "Luganda Inference on Gemma 3",
    "description": "Running an inference in Luganda on the Gemma 3 models for image and video tasks",
    "keywords": null,
    "author": null,
    "og:title": "Luganda Inference on Gemma 3",
    "og:description": "Running an inference in Luganda on the Gemma 3 models for image and video tasks",
    "og:url": "https://kambale.dev/luganda-inference-on-gemma-3",
    "og:site_name": "Wesley Kambale",
    "og:image": "https://cdn.hashnode.com/res/hashnode/image/upload/v1742224330570/5d528037-10cd-4d76-ad5b-a750bad19675.png",
    "og:image:width": "1200",
    "og:image:height": "630",
    "og:type": "article",
    "twitter:card": "summary_large_image",
    "twitter:creator": "https://twitter.com/WesKambale",
    "twitter:title": "Luganda Inference on Gemma 3",
    "twitter:description": "Running an inference in Luganda on the Gemma 3 models for image and video tasks",
    "twitter:image": "https://cdn.hashnode.com/res/hashnode/image/upload/v1742224330570/5d528037-10cd-4d76-ad5b-a750bad19675.png",
    "article:published_time": "2025-03-17T15:12:41.080Z",
    "article:modified_time": "2025-03-17T15:25:32.674Z",
    "article:author": "https://kambale.dev/@kambale"
  },
  "paywall": {
    "listed_paid": false,
    "detected": false,
    "marker": "",
    "public_only": false,
    "policy": "付费/会员内容只保留页面公开可见部分，不绕过付费墙"
  },
  "crawl": {
    "success": true,
    "status_code": 200,
    "error": ""
  },
  "code_blocks": [
    {
      "file": "attachments/adb8b16dc257/code/body-000.txt",
      "chars": 77,
      "language": "text"
    },
    {
      "file": "attachments/adb8b16dc257/code/body-001.txt",
      "chars": 205,
      "language": "text"
    },
    {
      "file": "attachments/adb8b16dc257/code/body-002.txt",
      "chars": 260,
      "language": "text"
    },
    {
      "file": "attachments/adb8b16dc257/code/body-003.txt",
      "chars": 2872,
      "language": "text"
    },
    {
      "file": "attachments/adb8b16dc257/code/body-004.txt",
      "chars": 504,
      "language": "text"
    },
    {
      "file": "attachments/adb8b16dc257/code/body-005.txt",
      "chars": 211,
      "language": "text"
    },
    {
      "file": "attachments/adb8b16dc257/code/body-006.txt",
      "chars": 11,
      "language": "text"
    },
    {
      "file": "attachments/adb8b16dc257/code/body-007.txt",
      "chars": 230,
      "language": "text"
    },
    {
      "file": "attachments/adb8b16dc257/code/body-008.txt",
      "chars": 9,
      "language": "text"
    },
    {
      "file": "attachments/adb8b16dc257/code/body-009.txt",
      "chars": 230,
      "language": "text"
    },
    {
      "file": "attachments/adb8b16dc257/code/body-010.txt",
      "chars": 16,
      "language": "text"
    },
    {
      "file": "attachments/adb8b16dc257/code/body-011.txt",
      "chars": 259,
      "language": "text"
    },
    {
      "file": "attachments/adb8b16dc257/code/body-012.txt",
      "chars": 6,
      "language": "text"
    },
    {
      "file": "attachments/adb8b16dc257/code/body-013.txt",
      "chars": 182,
      "language": "text"
    },
    {
      "file": "attachments/adb8b16dc257/code/body-014.txt",
      "chars": 1135,
      "language": "text"
    },
    {
      "file": "attachments/adb8b16dc257/code/body-015.txt",
      "chars": 1145,
      "language": "text"
    }
  ],
  "images": [
    {
      "status": "ok",
      "url": "https://cdn.hashnode.com/res/hashnode/image/upload/v1742224299893/52f52d5b-aeff-46f3-a84a-c63c21db3b3c.png",
      "bytes": 56546,
      "reused": true,
      "local_path": "attachments/adb8b16dc257/images/000.png",
      "src": "https://cdn.hashnode.com/res/hashnode/image/upload/v1742224299893/52f52d5b-aeff-46f3-a84a-c63c21db3b3c.png",
      "alt": "Luganda Inference on Gemma 3",
      "downloaded": true
    },
    {
      "status": "ok",
      "url": "https://cdn.hashnode.com/res/hashnode/image/upload/v1741973322171/639309f6-cd16-4ac9-8c38-6a71883cafe5.jpeg",
      "bytes": 103732,
      "reused": true,
      "local_path": "attachments/adb8b16dc257/images/002.jpeg",
      "src": "https://cdn.hashnode.com/res/hashnode/image/upload/v1741973322171/639309f6-cd16-4ac9-8c38-6a71883cafe5.jpeg",
      "alt": "",
      "downloaded": true
    },
    {
      "status": "ok",
      "url": "https://cdn.hashnode.com/res/hashnode/image/upload/v1742221026408/d08c920b-36d2-4ceb-bae4-cd315cffc8c8.jpeg",
      "bytes": 27728,
      "reused": true,
      "local_path": "attachments/adb8b16dc257/images/003.jpeg",
      "src": "https://cdn.hashnode.com/res/hashnode/image/upload/v1742221026408/d08c920b-36d2-4ceb-bae4-cd315cffc8c8.jpeg",
      "alt": "",
      "downloaded": true
    },
    {
      "status": "ok",
      "url": "https://cdn.hashnode.com/res/hashnode/image/upload/v1742221137566/1d334f7e-2236-4903-9d79-079a022af5b7.jpeg",
      "bytes": 142086,
      "reused": true,
      "local_path": "attachments/adb8b16dc257/images/004.jpeg",
      "src": "https://cdn.hashnode.com/res/hashnode/image/upload/v1742221137566/1d334f7e-2236-4903-9d79-079a022af5b7.jpeg",
      "alt": "",
      "downloaded": true
    },
    {
      "status": "ok",
      "url": "https://cdn.hashnode.com/res/hashnode/image/upload/v1742221248993/8ac94b19-069a-4ace-bc5a-338c2cb79125.png",
      "bytes": 220810,
      "reused": true,
      "local_path": "attachments/adb8b16dc257/images/005.png",
      "src": "https://cdn.hashnode.com/res/hashnode/image/upload/v1742221248993/8ac94b19-069a-4ace-bc5a-338c2cb79125.png",
      "alt": "",
      "downloaded": true
    }
  ],
  "audio": [],
  "videos": [
    {
      "url": "https://www.youtube.com/watch?v=u4D20WDrZyY",
      "src": "https://youtu.be/u4D20WDrZyY?si=uigb9f9hjCFjFxPV",
      "status": "ok",
      "kind": "single_video",
      "local_path": "attachments/adb8b16dc257/videos/repair-u4D20WDrZyY.mp4",
      "bytes": 72038738,
      "valid": true,
      "height": 1080,
      "width": 1920,
      "duration": "132.066667",
      "reused": false
    }
  ],
  "links": [
    {
      "href": "https://kambale.dev/luganda-inference-on-gemma-3.md",
      "label": "View as Markdown",
      "type": "a"
    },
    {
      "href": "https://hashnode.com/@kambale",
      "label": "W",
      "type": "a"
    },
    {
      "href": "https://twitter.com/WesKambale",
      "label": "",
      "type": "a"
    },
    {
      "href": "https://linkedin.com/in/WesKambale",
      "label": "",
      "type": "a"
    },
    {
      "href": "https://kambale.dev/series/ml-series",
      "label": "Part of series Machine Learning",
      "type": "a"
    },
    {
      "href": "https://www.google.com/url?q=https%3A%2F%2Fhuggingface.co",
      "label": "Hugging Face",
      "type": "a"
    },
    {
      "href": "https://www.google.com/url?q=https%3A%2F%2Fhuggingface.co%2Fgoogle%2Fgemma-3-4b-it",
      "label": "Gemma 3 model card",
      "type": "a"
    },
    {
      "href": "https://www.google.com/url?q=https%3A%2F%2Fhuggingface.co%2Fsettings",
      "label": "Settings page in Hugging Face",
      "type": "a"
    },
    {
      "href": "https://www.google.com/url?q=https%3A%2F%2Fyoutu.be%2Fu4D20WDrZyY%3Fsi%3DCU03hErvHfHCtxaX",
      "label": "YouTube",
      "type": "a"
    },
    {
      "href": "https://youtu.be/u4D20WDrZyY?si=uigb9f9hjCFjFxPV",
      "label": "https://youtu.be/u4D20WDrZyY?si=uigb9f9hjCFjFxPV",
      "type": "a"
    },
    {
      "href": "https://colab.research.google.com/drive/1xkB4O3yjjDEnd4x2yZCy56MbmK_OuASn?usp=sharing",
      "label": "notebook",
      "type": "a"
    },
    {
      "href": "https://kambale.dev/tag/luganda",
      "label": "# luganda",
      "type": "a"
    },
    {
      "href": "https://kambale.dev/tag/gemma",
      "label": "# gemma",
      "type": "a"
    },
    {
      "href": "https://kambale.dev/tag/genai",
      "label": "# genai",
      "type": "a"
    },
    {
      "href": "https://kambale.dev/tag/inference",
      "label": "# inference",
      "type": "a"
    }
  ],
  "repair_version": 2,
  "repaired_at": "2026-10-14T10:50:48Z",
  "source_url": "https://kambale.dev/luganda-inference-on-gemma-3",
  "papers": [],
  "actual_video_files": [],
  "body_chars": 12191,
  "expected_body_images": 5,
  "discovery_status": "ok",
  "delivery_status": "ok",
  "missing_reason": "",
  "raw_html": "attachments/adb8b16dc257/pages/source.html",
  "local_html": "attachments/adb8b16dc257/pages/article.html",
  "body_base_path": "attachments/adb8b16dc257/pages/",
  "body_markdown_path": "attachments/adb8b16dc257/pages/article.md"
}