<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/">
	<channel>
		<title><![CDATA[Python Forum - All Forums]]></title>
		<link>https://python-forum.io/</link>
		<description><![CDATA[Python Forum - https://python-forum.io]]></description>
		<pubDate>Tue, 09 Jun 2026 15:03:37 +0000</pubDate>
		<generator>MyBB</generator>
		<item>
			<title><![CDATA[How Yaar Win Makes Mobile Gaming More Accessible]]></title>
			<link>https://python-forum.io/thread-46331.html</link>
			<pubDate>Tue, 09 Jun 2026 09:45:31 +0000</pubDate>
			<dc:creator><![CDATA[<a href="https://python-forum.io/member.php?action=profile&uid=43902">viratsingh</a>]]></dc:creator>
			<guid isPermaLink="false">https://python-forum.io/thread-46331.html</guid>
			<description><![CDATA[Mobile gaming is becoming more popular because people want quick and easy entertainment on their phones. Platforms that focus on simple navigation, smooth performance, and user-friendly features often attract more players. <a href="https://yaarwin01.com/" target="_blank" rel="noopener" class="mycode_url">YaarWin login</a> helps users access their accounts quickly, making it easier to continue their gaming experience without unnecessary steps. As mobile technology improves, accessibility and convenience remain key factors in keeping players engaged. What features do you think make a mobile gaming platform more accessible and enjoyable for everyday users?]]></description>
			<content:encoded><![CDATA[Mobile gaming is becoming more popular because people want quick and easy entertainment on their phones. Platforms that focus on simple navigation, smooth performance, and user-friendly features often attract more players. <a href="https://yaarwin01.com/" target="_blank" rel="noopener" class="mycode_url">YaarWin login</a> helps users access their accounts quickly, making it easier to continue their gaming experience without unnecessary steps. As mobile technology improves, accessibility and convenience remain key factors in keeping players engaged. What features do you think make a mobile gaming platform more accessible and enjoyable for everyday users?]]></content:encoded>
		</item>
		<item>
			<title><![CDATA[Automated filler for an Excel form is not writing the data]]></title>
			<link>https://python-forum.io/thread-46329.html</link>
			<pubDate>Mon, 08 Jun 2026 02:02:31 +0000</pubDate>
			<dc:creator><![CDATA[<a href="https://python-forum.io/member.php?action=profile&uid=43896">Quian34</a>]]></dc:creator>
			<guid isPermaLink="false">https://python-forum.io/thread-46329.html</guid>
			<description><![CDATA[As the Tittle says, i have been fighting with this Python program written by a novice like me over several weeks. I need some help figuring out why I can't get it to write data from Excel/PDF documents to the output document.<br /><!-- start: postbit_attachments_attachment -->
<br /><!-- start: attachment_icon -->
<img src="https://python-forum.io/images/attachtypes/" title="Python source code" border="0" alt=".py" />
<!-- end: attachment_icon -->&nbsp;&nbsp;<a href="attachment.php?aid=3652" target="_blank" title="">Formu_V3.py</a> (Size: 22.98 KB / Downloads: 2)
<!-- end: postbit_attachments_attachment -->]]></description>
			<content:encoded><![CDATA[As the Tittle says, i have been fighting with this Python program written by a novice like me over several weeks. I need some help figuring out why I can't get it to write data from Excel/PDF documents to the output document.<br /><!-- start: postbit_attachments_attachment -->
<br /><!-- start: attachment_icon -->
<img src="https://python-forum.io/images/attachtypes/" title="Python source code" border="0" alt=".py" />
<!-- end: attachment_icon -->&nbsp;&nbsp;<a href="attachment.php?aid=3652" target="_blank" title="">Formu_V3.py</a> (Size: 22.98 KB / Downloads: 2)
<!-- end: postbit_attachments_attachment -->]]></content:encoded>
		</item>
		<item>
			<title><![CDATA[module not found error]]></title>
			<link>https://python-forum.io/thread-46327.html</link>
			<pubDate>Thu, 04 Jun 2026 22:56:52 +0000</pubDate>
			<dc:creator><![CDATA[<a href="https://python-forum.io/member.php?action=profile&uid=3079">Pedroski55</a>]]></dc:creator>
			<guid isPermaLink="false">https://python-forum.io/thread-46327.html</guid>
			<description><![CDATA[I have never used pygame, but I want to try out a little game I am making, so I installed pygame in my VENV called GPE.<br />
<br />
<blockquote class="mycode_quote"><cite>Quote:</cite>(GPE) peterr@peterr-Modern-15-B7M:~/PVE&#36; pip install pygame<br />
Requirement already satisfied: pygame in ./GPE/lib/python3.12/site-packages (2.6.1)<br />
(GPE) peterr@peterr-Modern-15-B7M:~/PVE&#36; </blockquote>
<br />
Try to run the game, says no module pygame:<br />
<br />
<blockquote class="mycode_quote"><cite>Quote:</cite>(GPE) peterr@peterr-Modern-15-B7M:~/PVE&#36; games/versuch1.py<br />
Traceback (most recent call last):<br />
  File "/home/peterr/PVE/games/versuch1.py", line 3, in &lt;module&gt;<br />
    import pygame<br />
ModuleNotFoundError: No module named 'pygame'<br />
(GPE) peterr@peterr-Modern-15-B7M:~/PVE&#36; </blockquote>
<br />
My VENV GPE is active<br />
<br />
I think maybe my shebang is wrong? I never installed pygame or anything else in the system Python, only in my VENV, GPE.<br />
<br />
This shebang at the top of versuch1.py does not work. pygame is only installed in my VENV. (I made my little game executable, so it should run in bash.)<br />
<br />
<blockquote class="mycode_quote"><cite>Quote:</cite>#! /usr/bin/python3</blockquote>
<br />
I tried pointing to this symlink in the VENV as the shebang:<br />
<br />
<blockquote class="mycode_quote"><cite>Quote:</cite>#! /home/peterr/PVE/GPE/python3.12/bin/python3</blockquote>
<br />
And tried this:<br />
<br />
<blockquote class="mycode_quote"><cite>Quote:</cite>#! GPE/python3.12/bin/python3</blockquote>
<br />
Any tips please? Is the shebang the problem??<br />
<br />
If I start Idle, I can import pygame, no problem.<br />
<br />
<pre class="brush: python" title="Python Code:">import pygame
pygame 2.6.1 (SDL 2.28.4, Python 3.12.3)
Hello from the pygame community. https://www.pygame.org/contribute.html</pre>pygame is installed in:<br />
<br />
<blockquote class="mycode_quote"><cite>Quote:</cite>GPE/lib/python3.12/site-packages</blockquote>
<br />
I have not had this trouble with other packages, as far as I remember.]]></description>
			<content:encoded><![CDATA[I have never used pygame, but I want to try out a little game I am making, so I installed pygame in my VENV called GPE.<br />
<br />
<blockquote class="mycode_quote"><cite>Quote:</cite>(GPE) peterr@peterr-Modern-15-B7M:~/PVE&#36; pip install pygame<br />
Requirement already satisfied: pygame in ./GPE/lib/python3.12/site-packages (2.6.1)<br />
(GPE) peterr@peterr-Modern-15-B7M:~/PVE&#36; </blockquote>
<br />
Try to run the game, says no module pygame:<br />
<br />
<blockquote class="mycode_quote"><cite>Quote:</cite>(GPE) peterr@peterr-Modern-15-B7M:~/PVE&#36; games/versuch1.py<br />
Traceback (most recent call last):<br />
  File "/home/peterr/PVE/games/versuch1.py", line 3, in &lt;module&gt;<br />
    import pygame<br />
ModuleNotFoundError: No module named 'pygame'<br />
(GPE) peterr@peterr-Modern-15-B7M:~/PVE&#36; </blockquote>
<br />
My VENV GPE is active<br />
<br />
I think maybe my shebang is wrong? I never installed pygame or anything else in the system Python, only in my VENV, GPE.<br />
<br />
This shebang at the top of versuch1.py does not work. pygame is only installed in my VENV. (I made my little game executable, so it should run in bash.)<br />
<br />
<blockquote class="mycode_quote"><cite>Quote:</cite>#! /usr/bin/python3</blockquote>
<br />
I tried pointing to this symlink in the VENV as the shebang:<br />
<br />
<blockquote class="mycode_quote"><cite>Quote:</cite>#! /home/peterr/PVE/GPE/python3.12/bin/python3</blockquote>
<br />
And tried this:<br />
<br />
<blockquote class="mycode_quote"><cite>Quote:</cite>#! GPE/python3.12/bin/python3</blockquote>
<br />
Any tips please? Is the shebang the problem??<br />
<br />
If I start Idle, I can import pygame, no problem.<br />
<br />
<pre class="brush: python" title="Python Code:">import pygame
pygame 2.6.1 (SDL 2.28.4, Python 3.12.3)
Hello from the pygame community. https://www.pygame.org/contribute.html</pre>pygame is installed in:<br />
<br />
<blockquote class="mycode_quote"><cite>Quote:</cite>GPE/lib/python3.12/site-packages</blockquote>
<br />
I have not had this trouble with other packages, as far as I remember.]]></content:encoded>
		</item>
		<item>
			<title><![CDATA[Has anyone ever used GetMessageW as a thread messaging system?]]></title>
			<link>https://python-forum.io/thread-46326.html</link>
			<pubDate>Wed, 03 Jun 2026 14:57:46 +0000</pubDate>
			<dc:creator><![CDATA[<a href="https://python-forum.io/member.php?action=profile&uid=36480">phpjunkie</a>]]></dc:creator>
			<guid isPermaLink="false">https://python-forum.io/thread-46326.html</guid>
			<description><![CDATA[Has anyone ever used <a href="https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getmessagew" target="_blank" rel="noopener" class="mycode_url">GetMessageW</a> to talk to threads in a threading system? I saw this in <code class="icode">pynput</code> and found it interesting.<br />
<pre class="brush: python" title="Python Code:">class MessageLoop(object):
    """A class representing a message loop."""

    #: The message that signals this loop to terminate
    WM_STOP = 0x0401

    _LPMSG = ctypes.POINTER(wintypes.MSG)

    _GetMessage = windll.user32.GetMessageW
    _GetMessage.argtypes = (
        ctypes.c_voidp,  # Really _LPMSG
        wintypes.HWND,
        wintypes.UINT,
        wintypes.UINT,
    )
    _PeekMessage = windll.user32.PeekMessageW
    _PeekMessage.argtypes = (
        ctypes.c_voidp,  # Really _LPMSG
        wintypes.HWND,
        wintypes.UINT,
        wintypes.UINT,
        wintypes.UINT,
    )
    _PostThreadMessage = windll.user32.PostThreadMessageW
    _PostThreadMessage.argtypes = (
        wintypes.DWORD,
        wintypes.UINT,
        wintypes.WPARAM,
        wintypes.LPARAM,
    )

    PM_NOREMOVE = 0

    def __init__(self):
        self._threadid = None
        self._event = threading.Event()
        self.thread = None

    def __iter__(self):
        """Initialises the message loop and yields all messages until
        :meth:`stop` is called.

        :raises AssertionError: if :meth:`start` has not been called
        """
        assert self._threadid is not None

        try:
            # Pump messages until WM_STOP
            while True:
                msg = wintypes.MSG()
                lpmsg = ctypes.byref(msg)
                r = self._GetMessage(lpmsg, None, 0, 0)
                if r &lt;= 0 or msg.message == self.WM_STOP:
                    break
                else:
                    yield msg

        finally:
            self._threadid = None
            self.thread = None

    def start(self):
        """Starts the message loop.

        This method must be called before iterating over messages, and it must
        be called from the same thread.
        """
        self._threadid = GetCurrentThreadId()
        self.thread = threading.current_thread()

        # Create the message loop
        msg = wintypes.MSG()
        lpmsg = ctypes.byref(msg)
        self._PeekMessage(lpmsg, None, 0x0400, 0x0400, self.PM_NOREMOVE)

        # Set the event to signal to other threads that the loop is created
        self._event.set()

    def stop(self):
        """Stops the message loop."""
        self._event.wait()
        if self._threadid:
            self.post(self.WM_STOP, 0, 0)

    def post(self, msg, wparam, lparam):
        """Posts a message to this message loop.

        :param ctypes.wintypes.UINT msg: The message.

        :param ctypes.wintypes.WPARAM wparam: The value of ``wParam``.

        :param ctypes.wintypes.LPARAM lparam: The value of ``lParam``.
        """
        self._PostThreadMessage(self._threadid, msg, wparam, lparam)</pre>I played around with the idea of it and wrote this.<br />
<pre class="brush: python" title="Python Code:">import ctypes
import sys
from collections.abc import Callable, Generator
from ctypes import wintypes
from ctypes.wintypes import MSG
from enum import IntEnum
from threading import Thread, current_thread
from typing import Any


# @formatter:off
class WM(IntEnum):
    CLOSE         = 0x0010
    QUIT          = 0x0012
    TIMER         = 0x0113
    USER          = 0x0400

    APP           = 0x8000
    REGISTER      = APP + 1

    START         = APP + 2
    CLOSE_THREAD  = APP + 3

    PRINT         = APP + 4


class PM(IntEnum):
    NOREMOVE = 0


class CtrlType(IntEnum):
    CTRL_C_EVENT     = 0
    CTRL_BREAK_EVENT = 1
    CTRL_CLOSE_EVENT = 2
# @formatter:on


# Provides strict type signatures so static analyzers treat the ctypes bindings as native methods rather than opaque _NamedFuncPointer objects.
def override_api_stubs(cls):
    kernel32 = ctypes.WinDLL('kernel32', use_last_error = True)
    user32 = ctypes.WinDLL('user32', use_last_error = True)

    cls.phandler_routine = ctypes.WINFUNCTYPE(
        wintypes.BOOL,
        wintypes.DWORD
    )

    cls.set_console_ctrl_handler = kernel32.SetConsoleCtrlHandler
    cls.set_console_ctrl_handler.argtypes = (cls.phandler_routine, wintypes.BOOL)
    cls.set_console_ctrl_handler.restype = wintypes.BOOL

    cls.get_message = user32.GetMessageW
    cls.get_message.argtypes = (ctypes.c_void_p, wintypes.HWND, wintypes.UINT, wintypes.UINT)
    cls.get_message.restype = wintypes.INT

    cls.peek_message = user32.PeekMessageW
    cls.peek_message.argtypes = (ctypes.c_void_p, wintypes.HWND, wintypes.UINT, wintypes.UINT, wintypes.UINT)
    cls.peek_message.restype = wintypes.BOOL

    cls.post_thread_message = user32.PostThreadMessageW
    cls.post_thread_message.argtypes = (wintypes.DWORD, wintypes.UINT, wintypes.WPARAM, wintypes.LPARAM)
    cls.post_thread_message.restype = wintypes.BOOL

    cls.set_timer = user32.SetTimer
    cls.set_timer.argtypes = (wintypes.HWND, ctypes.c_size_t, wintypes.UINT, ctypes.c_void_p)
    cls.set_timer.restype = ctypes.c_size_t

    cls.kill_timer = user32.KillTimer
    cls.kill_timer.argtypes = (wintypes.HWND, ctypes.c_size_t)
    cls.kill_timer.restype = wintypes.BOOL

    return cls


@override_api_stubs
class APIMixin:
    # Provides strict type signatures so static analyzers treat the ctypes bindings as native methods rather than opaque _NamedFuncPointer objects.

    _thread: Thread

    def __iter__(self) -&gt; Generator[MSG, None, None]:
        try:

            msg = MSG()
            lpmsg = ctypes.byref(msg)

            # Windows uses lazy memory allocation for thread queues.
            # We query WM_USER to forcefully allocate the queue in RAM before listening.
            self.peek_message(
                lpmsg,
                None, WM.USER,
                WM.USER,
                PM.NOREMOVE
            )

            # GetMessageW acts as a zero-CPU thread blocker.
            # It sleeps natively at the OS level until message is received.
            while self.get_message(lpmsg, None, 0, 0) &gt; 0:
                if msg.message == WM.QUIT:
                    break

                else:
                    yield msg

        finally:
            self._thread = None

    def post(self, tid: int, msg: int, wparam: int, lparam: int):
        self.post_thread_message(tid, msg, wparam, lparam)

    def phandler_routine(self, func: Callable[[int], bool]) -&gt; Any: ...
    def set_console_ctrl_handler(self, handler_routine: Any, add: bool) -&gt; bool: ...
    def get_message(self, msg: Any, hwnd: None, msg_filter_min: int, msg_filter_max: int) -&gt; int: ...
    def peek_message(self, msg: Any, hwnd: int, msg_filter_min: int, msg_filter_max: int, remove_msg: int) -&gt; bool: ...
    def post_thread_message(self, thread_id: int, msg: int, wparam: int, lparam: int) -&gt; bool: ...
    def set_timer(self, hwnd: int, event_id: int, elapse: int, timer_func: Any) -&gt; int: ...
    def kill_timer(self, hwnd: int, event_id: int) -&gt; int: ...


class Supervisor(APIMixin):

    @property
    def children(self) -&gt; list[int]:
        return self._children

    def __init__(self):
        super().__init__()

        self._thread = current_thread()
        self._thread_name = self._thread.name

        self._children = []

        # Hooks into the console to intercept Ctrl+C/Close events for a clean teardown.
        self._handler_routine = self.phandler_routine(self.console_stop_event_handler)
        self.set_console_ctrl_handler(self._handler_routine, True)

    def spin(self):
        self._clear_terminal()
        self._hide_cursor()

        for i in range(10):
            Worker(main_tid = self._thread.native_id, name = f'Worker {i + 1:02}').start()

        for msg in self:
            if msg.message == WM.REGISTER:
                self.register(msg.wParam)

            elif msg.message == WM.PRINT:
                text = self._extract_text(msg.wParam)

                print(f'Pulse sent from thread: {text!r}')

            elif msg.message == WM.CLOSE:
                self.close_children()

            elif msg.message == WM.CLOSE_THREAD:
                text = self._extract_text(msg.wParam)
                child_id = msg.lParam

                print(f'Closing {text!r}')

                self.send_quit(child_id)

                if len(self.children) == 0:
                    break

        print(f'Closing {self._thread_name!r}')
        self._show_cursor()

    def send_quit(self, tid: int):
        self.children.remove(tid)
        self.post(tid, WM.QUIT, 0, 0)

    def close(self):
        self.post(self._thread.native_id, WM.CLOSE, 0, 0)

    def close_children(self):
        for tid in self._children:
            self.post(tid, WM.CLOSE, 0, 0)

    def register(self, tid: int):
        self._children.append(tid)
        self.post(tid, WM.START, 0, 0)

    @staticmethod
    def _extract_text(memory_address: int) -&gt; str:
        return ctypes.wstring_at(memory_address)

    def _clear_terminal(self):
        self._write('\x1b[2J', f'\x1b[3J', f'\x1b[H')

    def _hide_cursor(self):
        self._write('\x1b[?25l')

    def _show_cursor(self):
        self._write('\x1b[?25h')

    @staticmethod
    def _write(*lines: str):
        for line in lines:
            sys.stdout.write(line)

        sys.stdout.flush()

    def console_stop_event_handler(self, ctrl_type: int) -&gt; bool:

        try:
            event = CtrlType(ctrl_type)

            print(f"\nShutdown signal detected {event.name!r}.\nIssuing a 'WM_CLOSE' message to main thread . . .\n")
            self.close()
            return True  # Tell Windows we handled the signal

        except ValueError:
            return False


class Worker(Thread, APIMixin):
    def __init__(self, main_tid: int, name: str):
        super(Worker, self).__init__(name = name)

        self._main_tid = main_tid
        self._timer: int = None

        self._name_buffer = ctypes.create_unicode_buffer(self.name)
        self._name_pointer = ctypes.addressof(self._name_buffer)

    def run(self):
        self.send_register()

        for msg in self:
            if msg.message == WM.START:
                self.start_timer()

            elif msg.message == WM.TIMER:
                self.send_print()

            elif msg.message == WM.CLOSE:
                self.send_closing()

    def send_print(self) -&gt; None:
        self.post(self._main_tid, WM.PRINT, self._name_pointer, 0)

    def send_closing(self):
        self.stop_timer()
        self.post(self._main_tid, WM.CLOSE_THREAD, self._name_pointer, self.native_id)

    def send_register(self):
        self.post(self._main_tid, WM.REGISTER, self.native_id, 0)

    def start_timer(self):
        self._timer = self.set_timer(None, 0, 1000, None)

    def stop_timer(self):
        self.kill_timer(None, self._timer)


if __name__ == '__main__':
    supervisor = Supervisor()
    supervisor.spin()</pre>]]></description>
			<content:encoded><![CDATA[Has anyone ever used <a href="https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getmessagew" target="_blank" rel="noopener" class="mycode_url">GetMessageW</a> to talk to threads in a threading system? I saw this in <code class="icode">pynput</code> and found it interesting.<br />
<pre class="brush: python" title="Python Code:">class MessageLoop(object):
    """A class representing a message loop."""

    #: The message that signals this loop to terminate
    WM_STOP = 0x0401

    _LPMSG = ctypes.POINTER(wintypes.MSG)

    _GetMessage = windll.user32.GetMessageW
    _GetMessage.argtypes = (
        ctypes.c_voidp,  # Really _LPMSG
        wintypes.HWND,
        wintypes.UINT,
        wintypes.UINT,
    )
    _PeekMessage = windll.user32.PeekMessageW
    _PeekMessage.argtypes = (
        ctypes.c_voidp,  # Really _LPMSG
        wintypes.HWND,
        wintypes.UINT,
        wintypes.UINT,
        wintypes.UINT,
    )
    _PostThreadMessage = windll.user32.PostThreadMessageW
    _PostThreadMessage.argtypes = (
        wintypes.DWORD,
        wintypes.UINT,
        wintypes.WPARAM,
        wintypes.LPARAM,
    )

    PM_NOREMOVE = 0

    def __init__(self):
        self._threadid = None
        self._event = threading.Event()
        self.thread = None

    def __iter__(self):
        """Initialises the message loop and yields all messages until
        :meth:`stop` is called.

        :raises AssertionError: if :meth:`start` has not been called
        """
        assert self._threadid is not None

        try:
            # Pump messages until WM_STOP
            while True:
                msg = wintypes.MSG()
                lpmsg = ctypes.byref(msg)
                r = self._GetMessage(lpmsg, None, 0, 0)
                if r &lt;= 0 or msg.message == self.WM_STOP:
                    break
                else:
                    yield msg

        finally:
            self._threadid = None
            self.thread = None

    def start(self):
        """Starts the message loop.

        This method must be called before iterating over messages, and it must
        be called from the same thread.
        """
        self._threadid = GetCurrentThreadId()
        self.thread = threading.current_thread()

        # Create the message loop
        msg = wintypes.MSG()
        lpmsg = ctypes.byref(msg)
        self._PeekMessage(lpmsg, None, 0x0400, 0x0400, self.PM_NOREMOVE)

        # Set the event to signal to other threads that the loop is created
        self._event.set()

    def stop(self):
        """Stops the message loop."""
        self._event.wait()
        if self._threadid:
            self.post(self.WM_STOP, 0, 0)

    def post(self, msg, wparam, lparam):
        """Posts a message to this message loop.

        :param ctypes.wintypes.UINT msg: The message.

        :param ctypes.wintypes.WPARAM wparam: The value of ``wParam``.

        :param ctypes.wintypes.LPARAM lparam: The value of ``lParam``.
        """
        self._PostThreadMessage(self._threadid, msg, wparam, lparam)</pre>I played around with the idea of it and wrote this.<br />
<pre class="brush: python" title="Python Code:">import ctypes
import sys
from collections.abc import Callable, Generator
from ctypes import wintypes
from ctypes.wintypes import MSG
from enum import IntEnum
from threading import Thread, current_thread
from typing import Any


# @formatter:off
class WM(IntEnum):
    CLOSE         = 0x0010
    QUIT          = 0x0012
    TIMER         = 0x0113
    USER          = 0x0400

    APP           = 0x8000
    REGISTER      = APP + 1

    START         = APP + 2
    CLOSE_THREAD  = APP + 3

    PRINT         = APP + 4


class PM(IntEnum):
    NOREMOVE = 0


class CtrlType(IntEnum):
    CTRL_C_EVENT     = 0
    CTRL_BREAK_EVENT = 1
    CTRL_CLOSE_EVENT = 2
# @formatter:on


# Provides strict type signatures so static analyzers treat the ctypes bindings as native methods rather than opaque _NamedFuncPointer objects.
def override_api_stubs(cls):
    kernel32 = ctypes.WinDLL('kernel32', use_last_error = True)
    user32 = ctypes.WinDLL('user32', use_last_error = True)

    cls.phandler_routine = ctypes.WINFUNCTYPE(
        wintypes.BOOL,
        wintypes.DWORD
    )

    cls.set_console_ctrl_handler = kernel32.SetConsoleCtrlHandler
    cls.set_console_ctrl_handler.argtypes = (cls.phandler_routine, wintypes.BOOL)
    cls.set_console_ctrl_handler.restype = wintypes.BOOL

    cls.get_message = user32.GetMessageW
    cls.get_message.argtypes = (ctypes.c_void_p, wintypes.HWND, wintypes.UINT, wintypes.UINT)
    cls.get_message.restype = wintypes.INT

    cls.peek_message = user32.PeekMessageW
    cls.peek_message.argtypes = (ctypes.c_void_p, wintypes.HWND, wintypes.UINT, wintypes.UINT, wintypes.UINT)
    cls.peek_message.restype = wintypes.BOOL

    cls.post_thread_message = user32.PostThreadMessageW
    cls.post_thread_message.argtypes = (wintypes.DWORD, wintypes.UINT, wintypes.WPARAM, wintypes.LPARAM)
    cls.post_thread_message.restype = wintypes.BOOL

    cls.set_timer = user32.SetTimer
    cls.set_timer.argtypes = (wintypes.HWND, ctypes.c_size_t, wintypes.UINT, ctypes.c_void_p)
    cls.set_timer.restype = ctypes.c_size_t

    cls.kill_timer = user32.KillTimer
    cls.kill_timer.argtypes = (wintypes.HWND, ctypes.c_size_t)
    cls.kill_timer.restype = wintypes.BOOL

    return cls


@override_api_stubs
class APIMixin:
    # Provides strict type signatures so static analyzers treat the ctypes bindings as native methods rather than opaque _NamedFuncPointer objects.

    _thread: Thread

    def __iter__(self) -&gt; Generator[MSG, None, None]:
        try:

            msg = MSG()
            lpmsg = ctypes.byref(msg)

            # Windows uses lazy memory allocation for thread queues.
            # We query WM_USER to forcefully allocate the queue in RAM before listening.
            self.peek_message(
                lpmsg,
                None, WM.USER,
                WM.USER,
                PM.NOREMOVE
            )

            # GetMessageW acts as a zero-CPU thread blocker.
            # It sleeps natively at the OS level until message is received.
            while self.get_message(lpmsg, None, 0, 0) &gt; 0:
                if msg.message == WM.QUIT:
                    break

                else:
                    yield msg

        finally:
            self._thread = None

    def post(self, tid: int, msg: int, wparam: int, lparam: int):
        self.post_thread_message(tid, msg, wparam, lparam)

    def phandler_routine(self, func: Callable[[int], bool]) -&gt; Any: ...
    def set_console_ctrl_handler(self, handler_routine: Any, add: bool) -&gt; bool: ...
    def get_message(self, msg: Any, hwnd: None, msg_filter_min: int, msg_filter_max: int) -&gt; int: ...
    def peek_message(self, msg: Any, hwnd: int, msg_filter_min: int, msg_filter_max: int, remove_msg: int) -&gt; bool: ...
    def post_thread_message(self, thread_id: int, msg: int, wparam: int, lparam: int) -&gt; bool: ...
    def set_timer(self, hwnd: int, event_id: int, elapse: int, timer_func: Any) -&gt; int: ...
    def kill_timer(self, hwnd: int, event_id: int) -&gt; int: ...


class Supervisor(APIMixin):

    @property
    def children(self) -&gt; list[int]:
        return self._children

    def __init__(self):
        super().__init__()

        self._thread = current_thread()
        self._thread_name = self._thread.name

        self._children = []

        # Hooks into the console to intercept Ctrl+C/Close events for a clean teardown.
        self._handler_routine = self.phandler_routine(self.console_stop_event_handler)
        self.set_console_ctrl_handler(self._handler_routine, True)

    def spin(self):
        self._clear_terminal()
        self._hide_cursor()

        for i in range(10):
            Worker(main_tid = self._thread.native_id, name = f'Worker {i + 1:02}').start()

        for msg in self:
            if msg.message == WM.REGISTER:
                self.register(msg.wParam)

            elif msg.message == WM.PRINT:
                text = self._extract_text(msg.wParam)

                print(f'Pulse sent from thread: {text!r}')

            elif msg.message == WM.CLOSE:
                self.close_children()

            elif msg.message == WM.CLOSE_THREAD:
                text = self._extract_text(msg.wParam)
                child_id = msg.lParam

                print(f'Closing {text!r}')

                self.send_quit(child_id)

                if len(self.children) == 0:
                    break

        print(f'Closing {self._thread_name!r}')
        self._show_cursor()

    def send_quit(self, tid: int):
        self.children.remove(tid)
        self.post(tid, WM.QUIT, 0, 0)

    def close(self):
        self.post(self._thread.native_id, WM.CLOSE, 0, 0)

    def close_children(self):
        for tid in self._children:
            self.post(tid, WM.CLOSE, 0, 0)

    def register(self, tid: int):
        self._children.append(tid)
        self.post(tid, WM.START, 0, 0)

    @staticmethod
    def _extract_text(memory_address: int) -&gt; str:
        return ctypes.wstring_at(memory_address)

    def _clear_terminal(self):
        self._write('\x1b[2J', f'\x1b[3J', f'\x1b[H')

    def _hide_cursor(self):
        self._write('\x1b[?25l')

    def _show_cursor(self):
        self._write('\x1b[?25h')

    @staticmethod
    def _write(*lines: str):
        for line in lines:
            sys.stdout.write(line)

        sys.stdout.flush()

    def console_stop_event_handler(self, ctrl_type: int) -&gt; bool:

        try:
            event = CtrlType(ctrl_type)

            print(f"\nShutdown signal detected {event.name!r}.\nIssuing a 'WM_CLOSE' message to main thread . . .\n")
            self.close()
            return True  # Tell Windows we handled the signal

        except ValueError:
            return False


class Worker(Thread, APIMixin):
    def __init__(self, main_tid: int, name: str):
        super(Worker, self).__init__(name = name)

        self._main_tid = main_tid
        self._timer: int = None

        self._name_buffer = ctypes.create_unicode_buffer(self.name)
        self._name_pointer = ctypes.addressof(self._name_buffer)

    def run(self):
        self.send_register()

        for msg in self:
            if msg.message == WM.START:
                self.start_timer()

            elif msg.message == WM.TIMER:
                self.send_print()

            elif msg.message == WM.CLOSE:
                self.send_closing()

    def send_print(self) -&gt; None:
        self.post(self._main_tid, WM.PRINT, self._name_pointer, 0)

    def send_closing(self):
        self.stop_timer()
        self.post(self._main_tid, WM.CLOSE_THREAD, self._name_pointer, self.native_id)

    def send_register(self):
        self.post(self._main_tid, WM.REGISTER, self.native_id, 0)

    def start_timer(self):
        self._timer = self.set_timer(None, 0, 1000, None)

    def stop_timer(self):
        self.kill_timer(None, self._timer)


if __name__ == '__main__':
    supervisor = Supervisor()
    supervisor.spin()</pre>]]></content:encoded>
		</item>
		<item>
			<title><![CDATA[Numpy, dash and em-dash]]></title>
			<link>https://python-forum.io/thread-46325.html</link>
			<pubDate>Tue, 02 Jun 2026 14:20:04 +0000</pubDate>
			<dc:creator><![CDATA[<a href="https://python-forum.io/member.php?action=profile&uid=43891">van60</a>]]></dc:creator>
			<guid isPermaLink="false">https://python-forum.io/thread-46325.html</guid>
			<description><![CDATA[I was trying to read a table with many columns in scientific notation into a numpy array. When I tried to cnvert the input strings into flating points numbers using:<br />
<br />
M_d1 = M_d1.astype(float)<br />
<br />
I got this error:<br />
<br />
ValueError: could not convert string to float: np.str_('1.01E−03')<br />
<br />
I took me a while to catch the issue: it lies in that little "−". It is not a dash ("-") , but an em-dash ("−"). I then changed all the occurrences of this em-dash in my table and now numpy runs smoothly, thus it does not recognize em-dash.]]></description>
			<content:encoded><![CDATA[I was trying to read a table with many columns in scientific notation into a numpy array. When I tried to cnvert the input strings into flating points numbers using:<br />
<br />
M_d1 = M_d1.astype(float)<br />
<br />
I got this error:<br />
<br />
ValueError: could not convert string to float: np.str_('1.01E−03')<br />
<br />
I took me a while to catch the issue: it lies in that little "−". It is not a dash ("-") , but an em-dash ("−"). I then changed all the occurrences of this em-dash in my table and now numpy runs smoothly, thus it does not recognize em-dash.]]></content:encoded>
		</item>
		<item>
			<title><![CDATA[Very new to python - help!]]></title>
			<link>https://python-forum.io/thread-46324.html</link>
			<pubDate>Mon, 01 Jun 2026 18:19:47 +0000</pubDate>
			<dc:creator><![CDATA[<a href="https://python-forum.io/member.php?action=profile&uid=43890">Jonesy95</a>]]></dc:creator>
			<guid isPermaLink="false">https://python-forum.io/thread-46324.html</guid>
			<description><![CDATA[Hi all,<br />
<br />
I'm just going through the "automate the boring stuff" workbook chapter 4. I cannot get my head around how "bacon local" is printed first? (Any help greatly appreciated!)<br />
<br />
<pre class="brush: python" title="Python Code:">def spam():
    eggs = 'spam local'
    print(eggs)  # Prints 'spam local'

def bacon():
    eggs = 'bacon local'
    print(eggs)  # Prints 'bacon local'
    spam()
    print(eggs)  # Prints 'bacon local'

eggs = 'global'
bacon()
print(eggs)  # Prints 'global'</pre>OUTPUT<br />
<br />
<pre><code class="codeblock output"><div class="title">Output:</div>bacon local
spam local
bacon local
global</code></pre>]]></description>
			<content:encoded><![CDATA[Hi all,<br />
<br />
I'm just going through the "automate the boring stuff" workbook chapter 4. I cannot get my head around how "bacon local" is printed first? (Any help greatly appreciated!)<br />
<br />
<pre class="brush: python" title="Python Code:">def spam():
    eggs = 'spam local'
    print(eggs)  # Prints 'spam local'

def bacon():
    eggs = 'bacon local'
    print(eggs)  # Prints 'bacon local'
    spam()
    print(eggs)  # Prints 'bacon local'

eggs = 'global'
bacon()
print(eggs)  # Prints 'global'</pre>OUTPUT<br />
<br />
<pre><code class="codeblock output"><div class="title">Output:</div>bacon local
spam local
bacon local
global</code></pre>]]></content:encoded>
		</item>
		<item>
			<title><![CDATA[[SOLVED] [Windows] Run "dir"?]]></title>
			<link>https://python-forum.io/thread-46323.html</link>
			<pubDate>Mon, 01 Jun 2026 13:41:27 +0000</pubDate>
			<dc:creator><![CDATA[<a href="https://python-forum.io/member.php?action=profile&uid=11957">Winfried</a>]]></dc:creator>
			<guid isPermaLink="false">https://python-forum.io/thread-46323.html</guid>
			<description><![CDATA[Hello,<br />
<br />
I can't figure out how to run "dir" in Windows. At this point, I get "Output:ERROR:None":<br />
<br />
<pre class="brush: python" title="Python Code:">CMD = "dir /AD /OD c:\\".rstrip()
CMD = r"dir /AD /OD c:".rstrip()
CMD = r"dir /AD /OD c:"
CMD = "dir /AD /OD c:"
print(CMD)

p = subprocess.run((['cmd.exe', '/c', 'dir', CMD]), text=True, stdout=subprocess.PIPE, creationflags=subprocess.CREATE_NO_WINDOW)
if p.returncode != 0:
	output = f"ERROR:{p.stderr}"
else:
	output = f"OK: {p.stdout}"
blah = input(f"Output:{output}")</pre>Does someone know?<br />
<br />
Thank you.<br />
<br />
--<br />
Edit: As a work-around, don't bother with Windows's dir command:<br />
<br />
<pre class="brush: python" title="Python Code:">import time, os

DIR=r"c:\temp"
dirs = [s for s in os.listdir(DIR) if os.path.isdir(os.path.join(DIR, s))]
for dir in dirs:
	print(dir, time.ctime(os.path.getmtime(dir)))</pre><pre class="brush: python" title="Python Code:">import time
from datetime import datetime, timezone
from pathlib import Path

paths = [subdir for subdir in Path(DIR).iterdir() if subdir.is_dir()]
paths = sorted(paths, key=os.path.getmtime)
for path in paths:
	stat_result = path.stat()
	modified = datetime.fromtimestamp(stat_result.st_mtime, tz=timezone.utc).strftime('%Y-%m-%d')
	print(f"{path} {modified}")</pre>]]></description>
			<content:encoded><![CDATA[Hello,<br />
<br />
I can't figure out how to run "dir" in Windows. At this point, I get "Output:ERROR:None":<br />
<br />
<pre class="brush: python" title="Python Code:">CMD = "dir /AD /OD c:\\".rstrip()
CMD = r"dir /AD /OD c:".rstrip()
CMD = r"dir /AD /OD c:"
CMD = "dir /AD /OD c:"
print(CMD)

p = subprocess.run((['cmd.exe', '/c', 'dir', CMD]), text=True, stdout=subprocess.PIPE, creationflags=subprocess.CREATE_NO_WINDOW)
if p.returncode != 0:
	output = f"ERROR:{p.stderr}"
else:
	output = f"OK: {p.stdout}"
blah = input(f"Output:{output}")</pre>Does someone know?<br />
<br />
Thank you.<br />
<br />
--<br />
Edit: As a work-around, don't bother with Windows's dir command:<br />
<br />
<pre class="brush: python" title="Python Code:">import time, os

DIR=r"c:\temp"
dirs = [s for s in os.listdir(DIR) if os.path.isdir(os.path.join(DIR, s))]
for dir in dirs:
	print(dir, time.ctime(os.path.getmtime(dir)))</pre><pre class="brush: python" title="Python Code:">import time
from datetime import datetime, timezone
from pathlib import Path

paths = [subdir for subdir in Path(DIR).iterdir() if subdir.is_dir()]
paths = sorted(paths, key=os.path.getmtime)
for path in paths:
	stat_result = path.stat()
	modified = datetime.fromtimestamp(stat_result.st_mtime, tz=timezone.utc).strftime('%Y-%m-%d')
	print(f"{path} {modified}")</pre>]]></content:encoded>
		</item>
		<item>
			<title><![CDATA[Is it possible to run Python 3.14.2 within Anaconda]]></title>
			<link>https://python-forum.io/thread-46322.html</link>
			<pubDate>Mon, 01 Jun 2026 10:07:55 +0000</pubDate>
			<dc:creator><![CDATA[<a href="https://python-forum.io/member.php?action=profile&uid=43889">neibalapython</a>]]></dc:creator>
			<guid isPermaLink="false">https://python-forum.io/thread-46322.html</guid>
			<description><![CDATA[Group<br />
<br />
How could I install Python 3.14.2 within Anaconda, considering which version is required and what the necessary requirements are, especially since I would be running Anaconda using version 3.14.2, mainly for study and script analysis purposes?]]></description>
			<content:encoded><![CDATA[Group<br />
<br />
How could I install Python 3.14.2 within Anaconda, considering which version is required and what the necessary requirements are, especially since I would be running Anaconda using version 3.14.2, mainly for study and script analysis purposes?]]></content:encoded>
		</item>
		<item>
			<title><![CDATA[INSTALL MANAGER]]></title>
			<link>https://python-forum.io/thread-46319.html</link>
			<pubDate>Mon, 25 May 2026 15:49:37 +0000</pubDate>
			<dc:creator><![CDATA[<a href="https://python-forum.io/member.php?action=profile&uid=43880">wrappitup123</a>]]></dc:creator>
			<guid isPermaLink="false">https://python-forum.io/thread-46319.html</guid>
			<description><![CDATA[How do i use install manager it is supposed to take me to a website to show me ll the latest version but it takes me to a crappy terminal, who made this crap?]]></description>
			<content:encoded><![CDATA[How do i use install manager it is supposed to take me to a website to show me ll the latest version but it takes me to a crappy terminal, who made this crap?]]></content:encoded>
		</item>
		<item>
			<title><![CDATA[Jarvis voice]]></title>
			<link>https://python-forum.io/thread-46318.html</link>
			<pubDate>Mon, 25 May 2026 13:22:11 +0000</pubDate>
			<dc:creator><![CDATA[<a href="https://python-forum.io/member.php?action=profile&uid=43870">Hoydenism</a>]]></dc:creator>
			<guid isPermaLink="false">https://python-forum.io/thread-46318.html</guid>
			<description><![CDATA[Hello guys, I'm currently developing my own assistant based on Jarvis from Iron Man. And now that I have a base, I want my assistant to talk with the same voice as Jarvis in the movie. I'm actually using edge-tts to make it speak.]]></description>
			<content:encoded><![CDATA[Hello guys, I'm currently developing my own assistant based on Jarvis from Iron Man. And now that I have a base, I want my assistant to talk with the same voice as Jarvis in the movie. I'm actually using edge-tts to make it speak.]]></content:encoded>
		</item>
		<item>
			<title><![CDATA[No prompts]]></title>
			<link>https://python-forum.io/thread-46317.html</link>
			<pubDate>Mon, 25 May 2026 13:10:10 +0000</pubDate>
			<dc:creator><![CDATA[<a href="https://python-forum.io/member.php?action=profile&uid=43880">wrappitup123</a>]]></dc:creator>
			<guid isPermaLink="false">https://python-forum.io/thread-46317.html</guid>
			<description><![CDATA[Hello I would like to use python in a friendly enviroment I don't want to use 1835 command prompts what software do I need?]]></description>
			<content:encoded><![CDATA[Hello I would like to use python in a friendly enviroment I don't want to use 1835 command prompts what software do I need?]]></content:encoded>
		</item>
		<item>
			<title><![CDATA[Mustatil - Pro - Yolo - Sam2 - Trainer - AI - Archeology]]></title>
			<link>https://python-forum.io/thread-46316.html</link>
			<pubDate>Fri, 22 May 2026 22:58:13 +0000</pubDate>
			<dc:creator><![CDATA[<a href="https://python-forum.io/member.php?action=profile&uid=43879">TWasfy</a>]]></dc:creator>
			<guid isPermaLink="false">https://python-forum.io/thread-46316.html</guid>
			<description><![CDATA[**A Yolo AI Training and detection tool with Sam2 segmentation and Formlearning**<br />
<br />
Pls try it out I am happy to get some feedback. What do you think of this?<br />
<br />
The installer .exe does download Python and all dependencys then it starts the GUI. Don't worry, it takes some time and should be an option for non Python natives. Download from Github releases or on itch.<br />
**Downloads:**<br />
[](<a href="https://github.com/11c1f717-7a94-4eee-ae8e-da9d427a757c" target="_blank" rel="noopener" class="mycode_url">https://github.com/11c1f717-7a94-4eee-ae8e-da9d427a757c</a>)<br />
**Download the .exe installer release from github or here:**<br />
[](<a href="https://tarekwasfy01.itch.io/mustatil-pro-yolo-sam2-ai-model-trainer-archeology" target="_blank" rel="noopener" class="mycode_url">https://tarekwasfy01.itch.io/mustatil-pr...archeology</a>)<br />
[<br />
<a href="https://github.com/tarekwasfy01/Mustatil---YOLO-AI-Model-Trainer-/releases/download/Mustatil-Pro-Yolo-Sam2-Trainer-V2/Mustatil_Setup.exe%5D(url)" target="_blank" rel="noopener" class="mycode_url">https://github.com/tarekwasfy01/Mustatil....exe](url)</a><br />
**For Python natives:**<br />
Download v Zip and click on the dependencies .bat. Use Python 3.11.<br />
**Download the Zip Python archive here:**<br />
[<a href="https://github.com/11c1f717-7a94-4eee-ae8e-da9d427a757c%5D()" target="_blank" rel="noopener" class="mycode_url">https://github.com/11c1f717-7a94-4eee-ae...27a757c]()</a><br />
**Download the .exe installer release from github or here:**<br />
[](<a href="https://tarekwasfy01.itch.io/mustatil-pro-yolo-sam2-ai-model-trainer-archeology" target="_blank" rel="noopener" class="mycode_url">https://tarekwasfy01.itch.io/mustatil-pr...archeology</a>)<br />
[<br />
<a href="https://github.com/tarekwasfy01/Mustatil---YOLO-AI-Model-Trainer-/releases/download/Mustatil-Pro-Yolo-Sam2-Trainer-V2/Mustatil_Setup.exe%5D(url)" target="_blank" rel="noopener" class="mycode_url">https://github.com/tarekwasfy01/Mustatil....exe](url)</a><br />
<br />
<img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r2a452jvyjktrv0qs0w7.png" loading="lazy"  alt="[Image: r2a452jvyjktrv0qs0w7.png]" class="mycode_img" />]]></description>
			<content:encoded><![CDATA[**A Yolo AI Training and detection tool with Sam2 segmentation and Formlearning**<br />
<br />
Pls try it out I am happy to get some feedback. What do you think of this?<br />
<br />
The installer .exe does download Python and all dependencys then it starts the GUI. Don't worry, it takes some time and should be an option for non Python natives. Download from Github releases or on itch.<br />
**Downloads:**<br />
[](<a href="https://github.com/11c1f717-7a94-4eee-ae8e-da9d427a757c" target="_blank" rel="noopener" class="mycode_url">https://github.com/11c1f717-7a94-4eee-ae8e-da9d427a757c</a>)<br />
**Download the .exe installer release from github or here:**<br />
[](<a href="https://tarekwasfy01.itch.io/mustatil-pro-yolo-sam2-ai-model-trainer-archeology" target="_blank" rel="noopener" class="mycode_url">https://tarekwasfy01.itch.io/mustatil-pr...archeology</a>)<br />
[<br />
<a href="https://github.com/tarekwasfy01/Mustatil---YOLO-AI-Model-Trainer-/releases/download/Mustatil-Pro-Yolo-Sam2-Trainer-V2/Mustatil_Setup.exe%5D(url)" target="_blank" rel="noopener" class="mycode_url">https://github.com/tarekwasfy01/Mustatil....exe](url)</a><br />
**For Python natives:**<br />
Download v Zip and click on the dependencies .bat. Use Python 3.11.<br />
**Download the Zip Python archive here:**<br />
[<a href="https://github.com/11c1f717-7a94-4eee-ae8e-da9d427a757c%5D()" target="_blank" rel="noopener" class="mycode_url">https://github.com/11c1f717-7a94-4eee-ae...27a757c]()</a><br />
**Download the .exe installer release from github or here:**<br />
[](<a href="https://tarekwasfy01.itch.io/mustatil-pro-yolo-sam2-ai-model-trainer-archeology" target="_blank" rel="noopener" class="mycode_url">https://tarekwasfy01.itch.io/mustatil-pr...archeology</a>)<br />
[<br />
<a href="https://github.com/tarekwasfy01/Mustatil---YOLO-AI-Model-Trainer-/releases/download/Mustatil-Pro-Yolo-Sam2-Trainer-V2/Mustatil_Setup.exe%5D(url)" target="_blank" rel="noopener" class="mycode_url">https://github.com/tarekwasfy01/Mustatil....exe](url)</a><br />
<br />
<img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r2a452jvyjktrv0qs0w7.png" loading="lazy"  alt="[Image: r2a452jvyjktrv0qs0w7.png]" class="mycode_img" />]]></content:encoded>
		</item>
		<item>
			<title><![CDATA[MarkdownCommander]]></title>
			<link>https://python-forum.io/thread-46307.html</link>
			<pubDate>Sat, 16 May 2026 01:36:31 +0000</pubDate>
			<dc:creator><![CDATA[<a href="https://python-forum.io/member.php?action=profile&uid=50">Larz60+</a>]]></dc:creator>
			<guid isPermaLink="false">https://python-forum.io/thread-46307.html</guid>
			<description><![CDATA[I have tons of markdown documents that i have collected for some time. Whenever I need to find one that I put together sometime in the past, the time consummed in actually finding the document often exceeded the value of it's contents. I decided a tool was needed that would allow quick access to any document, and perhaps display the results as well. So I came up with the idea for a Markdown Commander. I laid out the general plan, and decided I would use AI to help with the coding. My AI of choice is XGrok, and the contribution I received from Grok was tremendous. This turned out to be such a useful too, that I thought I'd share the code.<br />
<br />
So here's the steps needed to build it on your system. I built this on Linux Mint 21.3, and haven't tried it on any other OS, but I beleive it should run with little or no modification.<br />
<br />
<ol type="1" class="mycode_list"><li>Create a project directory and name it MarkdownCommander.<br />
</li>
<li>cd to that directory.<br />
</li>
<li>Create a virtual environment, using whatever tool you like. I use pythons venv like so: <code class="icode">python -m venv venv</code><br />
</li>
<li>Start the virtual environment: <code class="icode">. ./venv/bin/activate</code><br />
</li>
<li>install wxpython using the following command: <code class="icode">pip install -U -f https://extras.wxpython.org/wxPython4/extras/linux/gtk3/ubuntu-22.04 wxPython</code><br />
   This command is for Ubuntu system, using gtk which is what linux mint 21.3 uses as it's base. You may have to play with this for your OS. wxpython can be fussy.<br />
</li>
<li>Cleanup apt: <code class="icode">sudo apt autoclean</code><br />
</li>
<li>Update apt: <code class="icode">sudo apt update</code><br />
</li>
<li>Install wxwidgets using: <code class="icode">sudo apt install libwxgtk3.0-dev</code><br />
</li>
<li>Install mistune: <code class="icode">pip install mistune</code><br />
</li>
<li>install sentence_transformers: <code class="icode">pip install sentence_transformers</code><br />
</li>
<li>Create a src directory, and cd to that directory:<br />
    <code class="icode">mkdir src</code><br />
    <code class="icode">cd src</code><br />
</li>
<li>create an empty __init__.py file: <code class="icode">touch __init__.py</code><br />
</li>
<li>Using you favorite text editor, add MarkdownCommander.py:<br />
<pre class="brush: python" title="Python Code:">import wx
import wx.html2
import mistune
from pathlib import Path
from sentence_transformers import SentenceTransformer, util
import threading
import torch
from datetime import datetime


# ====================== SAFE TORCH LOAD ======================
torch.serialization.add_safe_globals([datetime])


class SemanticIndexer:
    def __init__(self, model_name="all-MiniLM-L6-v2"):
        self.model = SentenceTransformer(model_name)
        self.embeddings = None
        self.file_paths = []
        self.file_names = []
        self.file_mtimes = []
        self.source_dir = None
        self.build_time = None
        self.last_update_time = None
        self.exclude_dirs = {'.git', 'node_modules', '__pycache__', 'venv', 'env', '.venv'}

    def _get_file_info(self, path: Path):
        try:
            text = path.read_text(encoding='utf-8', errors='ignore')[:12000]
            stat = path.stat()
            return text, stat.st_mtime
        except:
            return None, None

    def build_index(self, directory: str):
        directory = Path(directory)
        self.source_dir = directory
        texts = []
        self.file_paths.clear()
        self.file_names.clear()
        self.file_mtimes.clear()

        for p in directory.rglob("*.*"):
            if any(ex in p.parts for ex in self.exclude_dirs):
                continue
            if p.suffix.lower() not in {'.md', '.txt', '.py', '.html', '.rst'}:
                continue
            text, mtime = self._get_file_info(p)
            if text:
                texts.append(text)
                self.file_paths.append(str(p))
                self.file_names.append(p.name)
                self.file_mtimes.append(mtime)

        print(f"Encoding {len(texts)} documents...")
        self.embeddings = self.model.encode(texts, convert_to_tensor=True, show_progress_bar=True)
        self.build_time = datetime.now()
        self.last_update_time = datetime.now()

    def search(self, query: str, top_k=10):
        if self.embeddings is None:
            return []
        query_emb = self.model.encode(query, convert_to_tensor=True)
        hits = util.semantic_search(query_emb, self.embeddings, top_k=top_k)[0]
        return [(self.file_paths[h['corpus_id']], 
                 self.file_names[h['corpus_id']], 
                 h['score']) for h in hits]

    def check_and_update(self):
        if not self.source_dir or self.embeddings is None:
            return False, 0

        current_files = {}
        for p in self.source_dir.rglob("*.*"):
            if any(ex in p.parts for ex in self.exclude_dirs):
                continue
            if p.suffix.lower() not in {'.md', '.txt', '.py', '.html', '.rst'}:
                continue
            _, mtime = self._get_file_info(p)
            if mtime:
                current_files[str(p)] = (p, mtime)

        changed_count = 0
        to_keep = [i for i, path in enumerate(self.file_paths) if path in current_files]
        if len(to_keep) != len(self.file_paths):
            changed_count += len(self.file_paths) - len(to_keep)
            self.file_paths = [self.file_paths[i] for i in to_keep]
            self.file_names = [self.file_names[i] for i in to_keep]
            self.file_mtimes = [self.file_mtimes[i] for i in to_keep]
            self.embeddings = self.embeddings[to_keep]

        existing_set = set(self.file_paths)
        to_add_texts = []
        to_add_paths = []
        to_add_names = []
        to_add_mtimes = []

        for path_str, (p, mtime) in current_files.items():
            if path_str not in existing_set:
                text, _ = self._get_file_info(p)
                if text:
                    to_add_texts.append(text)
                    to_add_paths.append(path_str)
                    to_add_names.append(p.name)
                    to_add_mtimes.append(mtime)
                    changed_count += 1
            else:
                idx = self.file_paths.index(path_str)
                if abs(mtime - self.file_mtimes[idx]) &gt; 2.0:
                    text, _ = self._get_file_info(p)
                    if text:
                        to_add_texts.append(text)
                        to_add_paths.append(path_str)
                        to_add_names.append(p.name)
                        to_add_mtimes.append(mtime)
                        self.file_paths.pop(idx)
                        self.file_names.pop(idx)
                        self.file_mtimes.pop(idx)
                        self.embeddings = torch.cat([self.embeddings[:idx], self.embeddings[idx+1:]])
                        changed_count += 1

        if to_add_texts:
            new_emb = self.model.encode(to_add_texts, convert_to_tensor=True, show_progress_bar=False)
            self.embeddings = torch.cat([self.embeddings, new_emb]) if len(self.embeddings) &gt; 0 else new_emb
            self.file_paths.extend(to_add_paths)
            self.file_names.extend(to_add_names)
            self.file_mtimes.extend(to_add_mtimes)

        if changed_count &gt; 0:
            self.last_update_time = datetime.now()

        return changed_count &gt; 0, changed_count

    def save(self, save_dir: Path):
        save_dir.mkdir(parents=True, exist_ok=True)
        torch.save({
            'embeddings': self.embeddings,
            'file_paths': self.file_paths,
            'file_names': self.file_names,
            'file_mtimes': self.file_mtimes,
            'source_dir': str(self.source_dir) if self.source_dir else None,
            'build_time': self.build_time,
            'last_update_time': self.last_update_time,
        }, save_dir / "index.pt")

    def load(self, save_dir: Path):
        data = torch.load(save_dir / "index.pt", weights_only=True, map_location='cpu')
        self.embeddings = data['embeddings']
        self.file_paths = data['file_paths']
        self.file_names = data['file_names']
        self.file_mtimes = data.get('file_mtimes', [])
        src = data.get('source_dir')
        self.source_dir = Path(src) if src else None
        self.build_time = data.get('build_time')
        self.last_update_time = data.get('last_update_time')


# ====================== MAIN APP ======================
class SemanticSearchApp(wx.Frame):
    INDEX_DIR = Path.home() / "semantic_search_index"
    DARK_BG = wx.Colour(30, 30, 30)
    DARK_FG = wx.Colour(230, 230, 230)
    LIGHT_BG = wx.Colour(255, 255, 255)
    LIGHT_FG = wx.Colour(0, 0, 0)

    def __init__(self):
        super().__init__(None, title="Markdown Commander", size=(1280, 820))
        self.indexer = None
        self.current_results = []
        self._update_lock = threading.Lock()
        self.is_dark_mode = False
        
        self._build_ui()
        self._try_auto_load_index()
        self.Show()

    def _build_ui(self):
        splitter = wx.SplitterWindow(self, style=wx.SP_LIVE_UPDATE | wx.SP_3D)
        left = wx.Panel(splitter)
        sizer = wx.BoxSizer(wx.VERTICAL)

        # Query area
        query_box = wx.BoxSizer(wx.HORIZONTAL)
        wx.StaticText(left, label="Query:", size=(50, -1))
        self.query_ctrl = wx.TextCtrl(left, style=wx.TE_PROCESS_ENTER, size=(-1, 40))
        self.query_ctrl.Bind(wx.EVT_TEXT_ENTER, self.on_search)
        query_box.Add(self.query_ctrl, 1, wx.EXPAND | wx.RIGHT, 8)

        self.search_btn = wx.Button(left, label="Search", size=(100, 40))
        self.search_btn.Bind(wx.EVT_BUTTON, self.on_search)
        query_box.Add(self.search_btn, 0, wx.ALIGN_CENTER_VERTICAL)

        sizer.Add(query_box, 0, wx.EXPAND | wx.ALL, 10)

        wx.StaticText(left, label="Results (double-click to open):")
        self.results_list = wx.ListCtrl(left, style=wx.LC_REPORT | wx.LC_SINGLE_SEL | wx.LC_HRULES)
        self.results_list.InsertColumn(0, "Score", width=90)
        self.results_list.InsertColumn(1, "Document", width=580)
        self.results_list.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.on_result_click)

        sizer.Add(self.results_list, 1, wx.EXPAND | wx.ALL, 10)
        left.SetSizer(sizer)

        # ==================== RIGHT PANEL - HTML PREVIEW ====================
        right_panel = wx.Panel(splitter)
        right_sizer = wx.BoxSizer(wx.VERTICAL)
        
        toolbar = wx.BoxSizer(wx.HORIZONTAL)
        self.clear_btn = wx.Button(right_panel, label="Clear Preview")
        self.clear_btn.Bind(wx.EVT_BUTTON, self.on_clear_preview)
        toolbar.Add(self.clear_btn, 0, wx.ALL, 5)
        right_sizer.Add(toolbar, 0, wx.EXPAND | wx.LEFT | wx.RIGHT, 5)

        # self.preview = wx.html.HtmlWindow(right_panel, style=wx.SUNKEN_BORDER)
        self.preview = wx.html2.WebView.New(right_panel)
        # self.preview.SetStandardFonts(10)
        right_sizer.Add(self.preview, 1, wx.EXPAND | wx.ALL, 5)
        
        right_panel.SetSizer(right_sizer)

        splitter.SplitVertically(left, right_panel, 680)
        splitter.SetMinimumPaneSize(400)

        # Menu
        menu_bar = wx.MenuBar()
        file_menu = wx.Menu()
        file_menu.Append(101, "&amp;Build/Rebuild Full Index...\tCtrl+B")
        file_menu.Append(102, "&amp;Force Full Rebuild\tCtrl+R")
        file_menu.Append(103, "&amp;Save Current Index\tCtrl+S")
        file_menu.AppendSeparator()
        file_menu.Append(105, "Toggle Dark/Light Mode\tCtrl+T")
        file_menu.AppendSeparator()
        file_menu.Append(104, "E&amp;xit")
        menu_bar.Append(file_menu, "&amp;File")
        self.SetMenuBar(menu_bar)

        self.Bind(wx.EVT_MENU, self.on_build_index, id=101)
        self.Bind(wx.EVT_MENU, self.on_force_rebuild, id=102)
        self.Bind(wx.EVT_MENU, self.on_save_index, id=103)
        self.Bind(wx.EVT_MENU, self.on_toggle_theme, id=105)
        self.Bind(wx.EVT_MENU, lambda e: self.Close(), id=104)

        self.SetStatusBar(wx.StatusBar(self))
        self.SetStatusText("Ready — Build an index to begin")
        self.apply_theme()

    def on_clear_preview(self, event):
        self.preview.SetPage("","")
        self.SetStatusText("Preview cleared")

    def apply_theme(self):
        bg = self.DARK_BG if self.is_dark_mode else self.LIGHT_BG
        fg = self.DARK_FG if self.is_dark_mode else self.LIGHT_FG

        self.SetBackgroundColour(bg)
        self.query_ctrl.SetBackgroundColour(bg)
        self.query_ctrl.SetForegroundColour(fg)
        self.results_list.SetBackgroundColour(bg)
        self.results_list.SetForegroundColour(fg)
        self.Refresh()
        self.Update()

    def on_toggle_theme(self, event):
        self.is_dark_mode = not self.is_dark_mode
        self.apply_theme()
        mode = "Dark" if self.is_dark_mode else "Light"
        self.SetStatusText(f"Switched to {mode} mode")

    # ==================== Remaining methods ====================
    def _try_auto_load_index(self):
        if not (self.INDEX_DIR / "index.pt").exists():
            return
        if wx.MessageBox("Saved index found. Load it now?", "Load Index", wx.YES_NO | wx.ICON_QUESTION) == wx.YES:
            try:
                self.indexer = SemanticIndexer()
                self.indexer.load(self.INDEX_DIR)
                self._index_ready()
            except Exception as e:
                wx.MessageBox(f"Load failed:\n{str(e)}", "Load Error", wx.OK | wx.ICON_ERROR)

    def on_build_index(self, event):
        dlg = wx.DirDialog(self, "Select folder with your documents")
        if dlg.ShowModal() == wx.ID_OK:
            path = dlg.GetPath()
            self.SetStatusText("Building full index...")
            threading.Thread(target=self._build_index_thread, args=(path,), daemon=True).start()
        dlg.Destroy()

    def _build_index_thread(self, folder):
        if self.indexer is None:
            self.indexer = SemanticIndexer()
        try:
            self.indexer.build_index(folder)
            wx.CallAfter(self._index_ready)
        except Exception as e:
            wx.CallAfter(lambda: wx.MessageBox(str(e), "Error"))

    def on_force_rebuild(self, event):
        if not self.indexer or not self.indexer.source_dir:
            wx.MessageBox("Please build an index first.", "Info")
            return
        if wx.MessageBox("Clear current index and rebuild everything?", "Force Rebuild", wx.YES_NO | wx.ICON_WARNING) == wx.YES:
            threading.Thread(target=self._build_index_thread, args=(str(self.indexer.source_dir),), daemon=True).start()

    def _index_ready(self):
        count = len(self.indexer.file_paths)
        ts = self.indexer.last_update_time.strftime("%Y-%m-%d %H:%M") if self.indexer.last_update_time else ""
        self.SetStatusText(f"Ready — {count} documents • Last updated: {ts}")

    def _check_for_updates(self):
        if not self.indexer:
            return
        with self._update_lock:
            updated, changed = self.indexer.check_and_update()
            if updated:
                wx.CallAfter(self._index_ready)
                wx.CallAfter(lambda: self.SetStatusText(f"Auto-updated: {changed} new/changed file{'s' if changed != 1 else ''}"))

    def on_search(self, event):
        if not self.indexer or self.indexer.embeddings is None:
            wx.MessageBox("Please build an index first.", "Info")
            return

        threading.Thread(target=self._check_for_updates, daemon=True).start()

        query = self.query_ctrl.GetValue().strip()
        if not query:
            return

        self.SetStatusText("Searching...")
        self.results_list.DeleteAllItems()

        results = self.indexer.search(query, top_k=12)
        self.current_results = results

        for i, (_, name, score) in enumerate(results):
            self.results_list.InsertItem(i, f"{score:.4f}")
            self.results_list.SetItem(i, 1, name)

        self.SetStatusText(f"Found {len(results)} results")

    def on_save_index(self, event):
        if not self.indexer or self.indexer.embeddings is None:
            wx.MessageBox("Nothing to save.", "Info")
            return
        try:
            self.indexer.save(self.INDEX_DIR)
            wx.MessageBox(f"Index saved to:\n{self.INDEX_DIR}", "Success")
        except Exception as e:
            wx.MessageBox(str(e), "Save Failed")

    def on_result_click(self, event):
        idx = event.GetIndex()
        full_path, name, score = self.current_results[idx]
        fpath = Path(full_path)
        
        try:
            content = fpath.read_text(encoding="utf-8", errors="ignore")
            html_content = mistune.html(content)            

            full_html = f"""
            &lt;html&gt;
            &lt;head&gt;
                &lt;style&gt;
                    body {{ font-family: Arial, Helvetica, sans-serif; padding: 20px; line-height: 1.6; }}
                    pre {{ background: #f4f4f4; padding: 12px; border-radius: 4px; overflow: auto; }}
                    code {{ font-family: monospace; }}
                    h1, h2, h3 {{ color: #2c3e50; }}
                    img {{ max-width: 100%; height: auto; display: block; margin: 15px 0; }}
                &lt;/style&gt;
            &lt;/head&gt;
            &lt;body&gt;
                {html_content}
            &lt;/body&gt;
            &lt;/html&gt;
            """

            # Fix: Use base URL so local images load
            base_url = f"file://{fpath.parent.resolve()}/"
            
            self.preview.SetPage(full_html, base_url)
            self.SetStatusText(f"Opened: {name}  (Score: {score:.4f})")
            
        except Exception as e:
            print(f"Preview error: {e}")  # for your console
            self.SetStatusText(f"Could not open file: {e}")
            self.preview.SetPage(f"&lt;html&gt;&lt;body&gt;&lt;p&gt;Error: {e}&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;", "")
        
if __name__ == "__main__":
    app = wx.App(False)
    frame = SemanticSearchApp()
    app.MainLoop()</pre></li>
<li>go back to main directory: <code class="icode">cd ..</code><br />
</li>
<li>create a data directory: <code class="icode">mkdir data</code><br />
</li>
<li>create a markdown sub directory in data: <code class="icode">mkdir ./data/markdown</code><br />
</li>
<li>And an image directory below the markdown directory: <code class="icode">mkdir ./data/markdown/images</code><br />
</li>
</ol>
<br />
Gather your markdown files, and load them all into the markdown directory, and any associated images into the images directory.<br />
<br />
sentence_transformers uses hugging face machine learning language, so an internet connection is needed to run.<br />
<br />
That should be all that's required to get started. <br />
<br />
From the project directory, run: <code class="icode">python src/MarkdownCommander.py</code><br />
<br />
Once you have built a model, it can be reloaded to speed up the process. You should only need to rebuild when you add new documents.<br />
<br />
Here's a screenshot of the program, and the display of a markdown document that contains images:<br />
<br />
<!-- start: postbit_attachments_attachment -->
<br /><!-- start: attachment_icon -->
<img src="https://python-forum.io/images/attachtypes/image.png" title="PNG Image" border="0" alt=".png" />
<!-- end: attachment_icon -->&nbsp;&nbsp;<a href="attachment.php?aid=3651" target="_blank" title="">ResultsPage.png</a> (Size: 253.55 KB / Downloads: 7)
<!-- end: postbit_attachments_attachment --><br />
<br />
Again, I greatly appreciate the tremendous help that I received from XGrok, which is my choice for python assistance.<br />
<br />
If you find that I missd anything, please let me know.<br />
<br />
Edited may 17 -- Clarified install steps]]></description>
			<content:encoded><![CDATA[I have tons of markdown documents that i have collected for some time. Whenever I need to find one that I put together sometime in the past, the time consummed in actually finding the document often exceeded the value of it's contents. I decided a tool was needed that would allow quick access to any document, and perhaps display the results as well. So I came up with the idea for a Markdown Commander. I laid out the general plan, and decided I would use AI to help with the coding. My AI of choice is XGrok, and the contribution I received from Grok was tremendous. This turned out to be such a useful too, that I thought I'd share the code.<br />
<br />
So here's the steps needed to build it on your system. I built this on Linux Mint 21.3, and haven't tried it on any other OS, but I beleive it should run with little or no modification.<br />
<br />
<ol type="1" class="mycode_list"><li>Create a project directory and name it MarkdownCommander.<br />
</li>
<li>cd to that directory.<br />
</li>
<li>Create a virtual environment, using whatever tool you like. I use pythons venv like so: <code class="icode">python -m venv venv</code><br />
</li>
<li>Start the virtual environment: <code class="icode">. ./venv/bin/activate</code><br />
</li>
<li>install wxpython using the following command: <code class="icode">pip install -U -f https://extras.wxpython.org/wxPython4/extras/linux/gtk3/ubuntu-22.04 wxPython</code><br />
   This command is for Ubuntu system, using gtk which is what linux mint 21.3 uses as it's base. You may have to play with this for your OS. wxpython can be fussy.<br />
</li>
<li>Cleanup apt: <code class="icode">sudo apt autoclean</code><br />
</li>
<li>Update apt: <code class="icode">sudo apt update</code><br />
</li>
<li>Install wxwidgets using: <code class="icode">sudo apt install libwxgtk3.0-dev</code><br />
</li>
<li>Install mistune: <code class="icode">pip install mistune</code><br />
</li>
<li>install sentence_transformers: <code class="icode">pip install sentence_transformers</code><br />
</li>
<li>Create a src directory, and cd to that directory:<br />
    <code class="icode">mkdir src</code><br />
    <code class="icode">cd src</code><br />
</li>
<li>create an empty __init__.py file: <code class="icode">touch __init__.py</code><br />
</li>
<li>Using you favorite text editor, add MarkdownCommander.py:<br />
<pre class="brush: python" title="Python Code:">import wx
import wx.html2
import mistune
from pathlib import Path
from sentence_transformers import SentenceTransformer, util
import threading
import torch
from datetime import datetime


# ====================== SAFE TORCH LOAD ======================
torch.serialization.add_safe_globals([datetime])


class SemanticIndexer:
    def __init__(self, model_name="all-MiniLM-L6-v2"):
        self.model = SentenceTransformer(model_name)
        self.embeddings = None
        self.file_paths = []
        self.file_names = []
        self.file_mtimes = []
        self.source_dir = None
        self.build_time = None
        self.last_update_time = None
        self.exclude_dirs = {'.git', 'node_modules', '__pycache__', 'venv', 'env', '.venv'}

    def _get_file_info(self, path: Path):
        try:
            text = path.read_text(encoding='utf-8', errors='ignore')[:12000]
            stat = path.stat()
            return text, stat.st_mtime
        except:
            return None, None

    def build_index(self, directory: str):
        directory = Path(directory)
        self.source_dir = directory
        texts = []
        self.file_paths.clear()
        self.file_names.clear()
        self.file_mtimes.clear()

        for p in directory.rglob("*.*"):
            if any(ex in p.parts for ex in self.exclude_dirs):
                continue
            if p.suffix.lower() not in {'.md', '.txt', '.py', '.html', '.rst'}:
                continue
            text, mtime = self._get_file_info(p)
            if text:
                texts.append(text)
                self.file_paths.append(str(p))
                self.file_names.append(p.name)
                self.file_mtimes.append(mtime)

        print(f"Encoding {len(texts)} documents...")
        self.embeddings = self.model.encode(texts, convert_to_tensor=True, show_progress_bar=True)
        self.build_time = datetime.now()
        self.last_update_time = datetime.now()

    def search(self, query: str, top_k=10):
        if self.embeddings is None:
            return []
        query_emb = self.model.encode(query, convert_to_tensor=True)
        hits = util.semantic_search(query_emb, self.embeddings, top_k=top_k)[0]
        return [(self.file_paths[h['corpus_id']], 
                 self.file_names[h['corpus_id']], 
                 h['score']) for h in hits]

    def check_and_update(self):
        if not self.source_dir or self.embeddings is None:
            return False, 0

        current_files = {}
        for p in self.source_dir.rglob("*.*"):
            if any(ex in p.parts for ex in self.exclude_dirs):
                continue
            if p.suffix.lower() not in {'.md', '.txt', '.py', '.html', '.rst'}:
                continue
            _, mtime = self._get_file_info(p)
            if mtime:
                current_files[str(p)] = (p, mtime)

        changed_count = 0
        to_keep = [i for i, path in enumerate(self.file_paths) if path in current_files]
        if len(to_keep) != len(self.file_paths):
            changed_count += len(self.file_paths) - len(to_keep)
            self.file_paths = [self.file_paths[i] for i in to_keep]
            self.file_names = [self.file_names[i] for i in to_keep]
            self.file_mtimes = [self.file_mtimes[i] for i in to_keep]
            self.embeddings = self.embeddings[to_keep]

        existing_set = set(self.file_paths)
        to_add_texts = []
        to_add_paths = []
        to_add_names = []
        to_add_mtimes = []

        for path_str, (p, mtime) in current_files.items():
            if path_str not in existing_set:
                text, _ = self._get_file_info(p)
                if text:
                    to_add_texts.append(text)
                    to_add_paths.append(path_str)
                    to_add_names.append(p.name)
                    to_add_mtimes.append(mtime)
                    changed_count += 1
            else:
                idx = self.file_paths.index(path_str)
                if abs(mtime - self.file_mtimes[idx]) &gt; 2.0:
                    text, _ = self._get_file_info(p)
                    if text:
                        to_add_texts.append(text)
                        to_add_paths.append(path_str)
                        to_add_names.append(p.name)
                        to_add_mtimes.append(mtime)
                        self.file_paths.pop(idx)
                        self.file_names.pop(idx)
                        self.file_mtimes.pop(idx)
                        self.embeddings = torch.cat([self.embeddings[:idx], self.embeddings[idx+1:]])
                        changed_count += 1

        if to_add_texts:
            new_emb = self.model.encode(to_add_texts, convert_to_tensor=True, show_progress_bar=False)
            self.embeddings = torch.cat([self.embeddings, new_emb]) if len(self.embeddings) &gt; 0 else new_emb
            self.file_paths.extend(to_add_paths)
            self.file_names.extend(to_add_names)
            self.file_mtimes.extend(to_add_mtimes)

        if changed_count &gt; 0:
            self.last_update_time = datetime.now()

        return changed_count &gt; 0, changed_count

    def save(self, save_dir: Path):
        save_dir.mkdir(parents=True, exist_ok=True)
        torch.save({
            'embeddings': self.embeddings,
            'file_paths': self.file_paths,
            'file_names': self.file_names,
            'file_mtimes': self.file_mtimes,
            'source_dir': str(self.source_dir) if self.source_dir else None,
            'build_time': self.build_time,
            'last_update_time': self.last_update_time,
        }, save_dir / "index.pt")

    def load(self, save_dir: Path):
        data = torch.load(save_dir / "index.pt", weights_only=True, map_location='cpu')
        self.embeddings = data['embeddings']
        self.file_paths = data['file_paths']
        self.file_names = data['file_names']
        self.file_mtimes = data.get('file_mtimes', [])
        src = data.get('source_dir')
        self.source_dir = Path(src) if src else None
        self.build_time = data.get('build_time')
        self.last_update_time = data.get('last_update_time')


# ====================== MAIN APP ======================
class SemanticSearchApp(wx.Frame):
    INDEX_DIR = Path.home() / "semantic_search_index"
    DARK_BG = wx.Colour(30, 30, 30)
    DARK_FG = wx.Colour(230, 230, 230)
    LIGHT_BG = wx.Colour(255, 255, 255)
    LIGHT_FG = wx.Colour(0, 0, 0)

    def __init__(self):
        super().__init__(None, title="Markdown Commander", size=(1280, 820))
        self.indexer = None
        self.current_results = []
        self._update_lock = threading.Lock()
        self.is_dark_mode = False
        
        self._build_ui()
        self._try_auto_load_index()
        self.Show()

    def _build_ui(self):
        splitter = wx.SplitterWindow(self, style=wx.SP_LIVE_UPDATE | wx.SP_3D)
        left = wx.Panel(splitter)
        sizer = wx.BoxSizer(wx.VERTICAL)

        # Query area
        query_box = wx.BoxSizer(wx.HORIZONTAL)
        wx.StaticText(left, label="Query:", size=(50, -1))
        self.query_ctrl = wx.TextCtrl(left, style=wx.TE_PROCESS_ENTER, size=(-1, 40))
        self.query_ctrl.Bind(wx.EVT_TEXT_ENTER, self.on_search)
        query_box.Add(self.query_ctrl, 1, wx.EXPAND | wx.RIGHT, 8)

        self.search_btn = wx.Button(left, label="Search", size=(100, 40))
        self.search_btn.Bind(wx.EVT_BUTTON, self.on_search)
        query_box.Add(self.search_btn, 0, wx.ALIGN_CENTER_VERTICAL)

        sizer.Add(query_box, 0, wx.EXPAND | wx.ALL, 10)

        wx.StaticText(left, label="Results (double-click to open):")
        self.results_list = wx.ListCtrl(left, style=wx.LC_REPORT | wx.LC_SINGLE_SEL | wx.LC_HRULES)
        self.results_list.InsertColumn(0, "Score", width=90)
        self.results_list.InsertColumn(1, "Document", width=580)
        self.results_list.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.on_result_click)

        sizer.Add(self.results_list, 1, wx.EXPAND | wx.ALL, 10)
        left.SetSizer(sizer)

        # ==================== RIGHT PANEL - HTML PREVIEW ====================
        right_panel = wx.Panel(splitter)
        right_sizer = wx.BoxSizer(wx.VERTICAL)
        
        toolbar = wx.BoxSizer(wx.HORIZONTAL)
        self.clear_btn = wx.Button(right_panel, label="Clear Preview")
        self.clear_btn.Bind(wx.EVT_BUTTON, self.on_clear_preview)
        toolbar.Add(self.clear_btn, 0, wx.ALL, 5)
        right_sizer.Add(toolbar, 0, wx.EXPAND | wx.LEFT | wx.RIGHT, 5)

        # self.preview = wx.html.HtmlWindow(right_panel, style=wx.SUNKEN_BORDER)
        self.preview = wx.html2.WebView.New(right_panel)
        # self.preview.SetStandardFonts(10)
        right_sizer.Add(self.preview, 1, wx.EXPAND | wx.ALL, 5)
        
        right_panel.SetSizer(right_sizer)

        splitter.SplitVertically(left, right_panel, 680)
        splitter.SetMinimumPaneSize(400)

        # Menu
        menu_bar = wx.MenuBar()
        file_menu = wx.Menu()
        file_menu.Append(101, "&amp;Build/Rebuild Full Index...\tCtrl+B")
        file_menu.Append(102, "&amp;Force Full Rebuild\tCtrl+R")
        file_menu.Append(103, "&amp;Save Current Index\tCtrl+S")
        file_menu.AppendSeparator()
        file_menu.Append(105, "Toggle Dark/Light Mode\tCtrl+T")
        file_menu.AppendSeparator()
        file_menu.Append(104, "E&amp;xit")
        menu_bar.Append(file_menu, "&amp;File")
        self.SetMenuBar(menu_bar)

        self.Bind(wx.EVT_MENU, self.on_build_index, id=101)
        self.Bind(wx.EVT_MENU, self.on_force_rebuild, id=102)
        self.Bind(wx.EVT_MENU, self.on_save_index, id=103)
        self.Bind(wx.EVT_MENU, self.on_toggle_theme, id=105)
        self.Bind(wx.EVT_MENU, lambda e: self.Close(), id=104)

        self.SetStatusBar(wx.StatusBar(self))
        self.SetStatusText("Ready — Build an index to begin")
        self.apply_theme()

    def on_clear_preview(self, event):
        self.preview.SetPage("","")
        self.SetStatusText("Preview cleared")

    def apply_theme(self):
        bg = self.DARK_BG if self.is_dark_mode else self.LIGHT_BG
        fg = self.DARK_FG if self.is_dark_mode else self.LIGHT_FG

        self.SetBackgroundColour(bg)
        self.query_ctrl.SetBackgroundColour(bg)
        self.query_ctrl.SetForegroundColour(fg)
        self.results_list.SetBackgroundColour(bg)
        self.results_list.SetForegroundColour(fg)
        self.Refresh()
        self.Update()

    def on_toggle_theme(self, event):
        self.is_dark_mode = not self.is_dark_mode
        self.apply_theme()
        mode = "Dark" if self.is_dark_mode else "Light"
        self.SetStatusText(f"Switched to {mode} mode")

    # ==================== Remaining methods ====================
    def _try_auto_load_index(self):
        if not (self.INDEX_DIR / "index.pt").exists():
            return
        if wx.MessageBox("Saved index found. Load it now?", "Load Index", wx.YES_NO | wx.ICON_QUESTION) == wx.YES:
            try:
                self.indexer = SemanticIndexer()
                self.indexer.load(self.INDEX_DIR)
                self._index_ready()
            except Exception as e:
                wx.MessageBox(f"Load failed:\n{str(e)}", "Load Error", wx.OK | wx.ICON_ERROR)

    def on_build_index(self, event):
        dlg = wx.DirDialog(self, "Select folder with your documents")
        if dlg.ShowModal() == wx.ID_OK:
            path = dlg.GetPath()
            self.SetStatusText("Building full index...")
            threading.Thread(target=self._build_index_thread, args=(path,), daemon=True).start()
        dlg.Destroy()

    def _build_index_thread(self, folder):
        if self.indexer is None:
            self.indexer = SemanticIndexer()
        try:
            self.indexer.build_index(folder)
            wx.CallAfter(self._index_ready)
        except Exception as e:
            wx.CallAfter(lambda: wx.MessageBox(str(e), "Error"))

    def on_force_rebuild(self, event):
        if not self.indexer or not self.indexer.source_dir:
            wx.MessageBox("Please build an index first.", "Info")
            return
        if wx.MessageBox("Clear current index and rebuild everything?", "Force Rebuild", wx.YES_NO | wx.ICON_WARNING) == wx.YES:
            threading.Thread(target=self._build_index_thread, args=(str(self.indexer.source_dir),), daemon=True).start()

    def _index_ready(self):
        count = len(self.indexer.file_paths)
        ts = self.indexer.last_update_time.strftime("%Y-%m-%d %H:%M") if self.indexer.last_update_time else ""
        self.SetStatusText(f"Ready — {count} documents • Last updated: {ts}")

    def _check_for_updates(self):
        if not self.indexer:
            return
        with self._update_lock:
            updated, changed = self.indexer.check_and_update()
            if updated:
                wx.CallAfter(self._index_ready)
                wx.CallAfter(lambda: self.SetStatusText(f"Auto-updated: {changed} new/changed file{'s' if changed != 1 else ''}"))

    def on_search(self, event):
        if not self.indexer or self.indexer.embeddings is None:
            wx.MessageBox("Please build an index first.", "Info")
            return

        threading.Thread(target=self._check_for_updates, daemon=True).start()

        query = self.query_ctrl.GetValue().strip()
        if not query:
            return

        self.SetStatusText("Searching...")
        self.results_list.DeleteAllItems()

        results = self.indexer.search(query, top_k=12)
        self.current_results = results

        for i, (_, name, score) in enumerate(results):
            self.results_list.InsertItem(i, f"{score:.4f}")
            self.results_list.SetItem(i, 1, name)

        self.SetStatusText(f"Found {len(results)} results")

    def on_save_index(self, event):
        if not self.indexer or self.indexer.embeddings is None:
            wx.MessageBox("Nothing to save.", "Info")
            return
        try:
            self.indexer.save(self.INDEX_DIR)
            wx.MessageBox(f"Index saved to:\n{self.INDEX_DIR}", "Success")
        except Exception as e:
            wx.MessageBox(str(e), "Save Failed")

    def on_result_click(self, event):
        idx = event.GetIndex()
        full_path, name, score = self.current_results[idx]
        fpath = Path(full_path)
        
        try:
            content = fpath.read_text(encoding="utf-8", errors="ignore")
            html_content = mistune.html(content)            

            full_html = f"""
            &lt;html&gt;
            &lt;head&gt;
                &lt;style&gt;
                    body {{ font-family: Arial, Helvetica, sans-serif; padding: 20px; line-height: 1.6; }}
                    pre {{ background: #f4f4f4; padding: 12px; border-radius: 4px; overflow: auto; }}
                    code {{ font-family: monospace; }}
                    h1, h2, h3 {{ color: #2c3e50; }}
                    img {{ max-width: 100%; height: auto; display: block; margin: 15px 0; }}
                &lt;/style&gt;
            &lt;/head&gt;
            &lt;body&gt;
                {html_content}
            &lt;/body&gt;
            &lt;/html&gt;
            """

            # Fix: Use base URL so local images load
            base_url = f"file://{fpath.parent.resolve()}/"
            
            self.preview.SetPage(full_html, base_url)
            self.SetStatusText(f"Opened: {name}  (Score: {score:.4f})")
            
        except Exception as e:
            print(f"Preview error: {e}")  # for your console
            self.SetStatusText(f"Could not open file: {e}")
            self.preview.SetPage(f"&lt;html&gt;&lt;body&gt;&lt;p&gt;Error: {e}&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;", "")
        
if __name__ == "__main__":
    app = wx.App(False)
    frame = SemanticSearchApp()
    app.MainLoop()</pre></li>
<li>go back to main directory: <code class="icode">cd ..</code><br />
</li>
<li>create a data directory: <code class="icode">mkdir data</code><br />
</li>
<li>create a markdown sub directory in data: <code class="icode">mkdir ./data/markdown</code><br />
</li>
<li>And an image directory below the markdown directory: <code class="icode">mkdir ./data/markdown/images</code><br />
</li>
</ol>
<br />
Gather your markdown files, and load them all into the markdown directory, and any associated images into the images directory.<br />
<br />
sentence_transformers uses hugging face machine learning language, so an internet connection is needed to run.<br />
<br />
That should be all that's required to get started. <br />
<br />
From the project directory, run: <code class="icode">python src/MarkdownCommander.py</code><br />
<br />
Once you have built a model, it can be reloaded to speed up the process. You should only need to rebuild when you add new documents.<br />
<br />
Here's a screenshot of the program, and the display of a markdown document that contains images:<br />
<br />
<!-- start: postbit_attachments_attachment -->
<br /><!-- start: attachment_icon -->
<img src="https://python-forum.io/images/attachtypes/image.png" title="PNG Image" border="0" alt=".png" />
<!-- end: attachment_icon -->&nbsp;&nbsp;<a href="attachment.php?aid=3651" target="_blank" title="">ResultsPage.png</a> (Size: 253.55 KB / Downloads: 7)
<!-- end: postbit_attachments_attachment --><br />
<br />
Again, I greatly appreciate the tremendous help that I received from XGrok, which is my choice for python assistance.<br />
<br />
If you find that I missd anything, please let me know.<br />
<br />
Edited may 17 -- Clarified install steps]]></content:encoded>
		</item>
		<item>
			<title><![CDATA[My First Python Script. "Auto VOD Trimmer"]]></title>
			<link>https://python-forum.io/thread-46305.html</link>
			<pubDate>Fri, 15 May 2026 21:18:32 +0000</pubDate>
			<dc:creator><![CDATA[<a href="https://python-forum.io/member.php?action=profile&uid=43872">DeegoFronk</a>]]></dc:creator>
			<guid isPermaLink="false">https://python-forum.io/thread-46305.html</guid>
			<description><![CDATA[<a href="https://github.com/DeegoFronk/Auto-Vod-Trimmer" target="_blank" rel="noopener" class="mycode_url">https://github.com/DeegoFronk/Auto-Vod-Trimmer</a><br />
<br />
Im a 32 year old, disabled, former Navy Sonar Technician, I do some hobby streaming with some friends on the side and i edit some videos for youtube from the VODS. I wanted to find a product that would allow me to trim the vods automatically, but the only solutions on the market are very strict on the upload length and heavy on the monthly fee. Ive dabbled in python and unreal engine in the past and taken some intro courses but never even touched a functional script or an LLM chat window before i started this project.<br />
<br />
I engineered the acoustic logic, the math, and the "Chaos Score" weights based on real-world testing, while forcing the AI via strict system prompts to act as my syntax translator and architectural soundboard. But i understand current Anti AI sentiments and myself still hold them for a lot of AI use cases, so that's why I'm very clearly and openly disclosing the AI element and the degree to which AI helped create this project.<br />
<br />
I used an LLM to help me architect a multi-threaded Python engine that glues together Faster-whisper and Librosa. Instead of just looking for loud noises, the engine calculates a "Chaos Score" using a Twin-Beam system for separate Game and mic audio scoring.<br />
<br />
It slices the VOD into 3-second chunks, runs on 8 concurrent threads to bypass Python's GIL, does all the math on 10MB chunked raw PCM data to prevent RAM overflow, and outputs a Trimmed VOD and a Highlight Reel, as well as a desired number for 120-180s high scoring clips for shorts, natively via FFmpeg.<br />
<br />
VOD Auto Trimmer Overview<br />
<br />
VOD Auto Trimmer is a multi-threaded Python engine designed to eliminate the manual labor of scrubbing through hours of stream footage. It uses localized AI speech-to-text and acoustic physics to autonomously analyze, rank, and edit raw VODs into ratio-controlled trimmed videos, highlight reels, and format-ready YouTube Shorts.<br />
<br />
This tool runs 100% locally. It does not rely on cloud APIs, ensuring zero recurring costs and absolute privacy for your raw footage.<br />
<br />
Acoustic Intelligence (Librosa): Processes audio across 8 CPU cores to calculate dynamic range (Crest Factor), high-frequency combat impacts (Spectral Centroid &gt; 1500Hz), and fundamental human pitch peaks (2000Hz+ for scream detection). It natively ignores static hiss and digital artifacts.<br />
<br />
Semantic Analysis (Faster-Whisper): Utilizes a 12-thread local AI model to transcribe microphone audio. It calculates Words-Per-Minute (WPM) to detect high-stress "Panic Modes," flags explicit hype words (e.g., "clip that"), and maps text back to 3-second acoustic chunks using word-level timestamps.<br />
<br />
The "Chaos Score" Algorithm: Ranks every 3-second segment of a VOD using a linear additive summation model. It weighs game volume, mic volume, combat triggers, linguistic intent, and WPM against a baseline floor to separate dead air from peak stream moments.<br />
<br />
Game Audio: Uses Harmonic-Percussive Source Separation and Spectral Centroids to detect high-frequency combat impacts (like gunshots), ignoring low rumbles and background music.<br />
<br />
Mic Audio: Uses true pitch detection to separate actual human screams (&gt;2000Hz) from digital hiss/artifacts. Meanwhile Whisper calculates Words-Per-Minute to detect "panic modes" and flags explicit<br />
user-defined hype words.<br />
<br />
<br />
Automated Output Generation:<br />
<br />
    Trimmed VOD: Prunes the lowest-scoring "dead air" until the video matches a user-defined target ratio (e.g., 50% of the original duration).<br />
<br />
    Highlight Reel: A tightly packed montage of the highest-scoring moments capped at a user-defined minute limit.<br />
<br />
    Shorts Module: Extracts the absolute highest-ranking events, enforcing a 120-180 second rule (center-cropping clips that are too long) for vertical short-form platforms.<br />
<br />
    Thumbnail Bursts: Automatically captures high-quality frames spread across the mathematical center of generated Shorts.<br />
<br />
Some feedback, advice, anything of the sort from a real human being outside myself and my streamer buddy who helped test it would be greatly appreciated thanks in advance to anyone who has the time to look at it.<br />
<br />
<a href="https://github.com/DeegoFronk/Auto-Vod-Trimmer" target="_blank" rel="noopener" class="mycode_url">Github</a><br />
<br />
My Current Road Map:<br />
<br />
 1. Docker / Linux / Mac Support: I built and deployed it strictly for Windows purely because that is the only hardware environment I currently have to test on. Wrapping it in Docker would be a much cleaner way to handle the FFmpeg dependencies and cross-platform pathing for Version 2.0.<br />
<br />
2. FFmpeg Split/Convert: Right now, the engine extracts the raw PCM float audio via an FFmpeg pipe to do the math, and then uses -c:v copy -c:a aac with a concat list for the final lossless render. Feeding it a non-standard codec or a weird .mkv wrapper will likely break that final concat step or cause audio drift.<br />
<br />
3. Benchmarks: I completely overlooked hardware benchmarking. The script currently outputs a ton of diagnostic stats to the terminal (Total runtime, Dead air trimmed, Peak WPM, Average Pitch, Chaos Scores) to help me tune the scoring math, but I haven't officially documented the hardware footprint beyond watching task manager and my CPU temps. For context, processing a 4-hour 1080p/60fps VOD on a standard 8-core CPU with 16GB RAM currently takes about 18-24 minutes. I estimate a full 6-hour VOD scales to roughly 30-35 minutes.]]></description>
			<content:encoded><![CDATA[<a href="https://github.com/DeegoFronk/Auto-Vod-Trimmer" target="_blank" rel="noopener" class="mycode_url">https://github.com/DeegoFronk/Auto-Vod-Trimmer</a><br />
<br />
Im a 32 year old, disabled, former Navy Sonar Technician, I do some hobby streaming with some friends on the side and i edit some videos for youtube from the VODS. I wanted to find a product that would allow me to trim the vods automatically, but the only solutions on the market are very strict on the upload length and heavy on the monthly fee. Ive dabbled in python and unreal engine in the past and taken some intro courses but never even touched a functional script or an LLM chat window before i started this project.<br />
<br />
I engineered the acoustic logic, the math, and the "Chaos Score" weights based on real-world testing, while forcing the AI via strict system prompts to act as my syntax translator and architectural soundboard. But i understand current Anti AI sentiments and myself still hold them for a lot of AI use cases, so that's why I'm very clearly and openly disclosing the AI element and the degree to which AI helped create this project.<br />
<br />
I used an LLM to help me architect a multi-threaded Python engine that glues together Faster-whisper and Librosa. Instead of just looking for loud noises, the engine calculates a "Chaos Score" using a Twin-Beam system for separate Game and mic audio scoring.<br />
<br />
It slices the VOD into 3-second chunks, runs on 8 concurrent threads to bypass Python's GIL, does all the math on 10MB chunked raw PCM data to prevent RAM overflow, and outputs a Trimmed VOD and a Highlight Reel, as well as a desired number for 120-180s high scoring clips for shorts, natively via FFmpeg.<br />
<br />
VOD Auto Trimmer Overview<br />
<br />
VOD Auto Trimmer is a multi-threaded Python engine designed to eliminate the manual labor of scrubbing through hours of stream footage. It uses localized AI speech-to-text and acoustic physics to autonomously analyze, rank, and edit raw VODs into ratio-controlled trimmed videos, highlight reels, and format-ready YouTube Shorts.<br />
<br />
This tool runs 100% locally. It does not rely on cloud APIs, ensuring zero recurring costs and absolute privacy for your raw footage.<br />
<br />
Acoustic Intelligence (Librosa): Processes audio across 8 CPU cores to calculate dynamic range (Crest Factor), high-frequency combat impacts (Spectral Centroid &gt; 1500Hz), and fundamental human pitch peaks (2000Hz+ for scream detection). It natively ignores static hiss and digital artifacts.<br />
<br />
Semantic Analysis (Faster-Whisper): Utilizes a 12-thread local AI model to transcribe microphone audio. It calculates Words-Per-Minute (WPM) to detect high-stress "Panic Modes," flags explicit hype words (e.g., "clip that"), and maps text back to 3-second acoustic chunks using word-level timestamps.<br />
<br />
The "Chaos Score" Algorithm: Ranks every 3-second segment of a VOD using a linear additive summation model. It weighs game volume, mic volume, combat triggers, linguistic intent, and WPM against a baseline floor to separate dead air from peak stream moments.<br />
<br />
Game Audio: Uses Harmonic-Percussive Source Separation and Spectral Centroids to detect high-frequency combat impacts (like gunshots), ignoring low rumbles and background music.<br />
<br />
Mic Audio: Uses true pitch detection to separate actual human screams (&gt;2000Hz) from digital hiss/artifacts. Meanwhile Whisper calculates Words-Per-Minute to detect "panic modes" and flags explicit<br />
user-defined hype words.<br />
<br />
<br />
Automated Output Generation:<br />
<br />
    Trimmed VOD: Prunes the lowest-scoring "dead air" until the video matches a user-defined target ratio (e.g., 50% of the original duration).<br />
<br />
    Highlight Reel: A tightly packed montage of the highest-scoring moments capped at a user-defined minute limit.<br />
<br />
    Shorts Module: Extracts the absolute highest-ranking events, enforcing a 120-180 second rule (center-cropping clips that are too long) for vertical short-form platforms.<br />
<br />
    Thumbnail Bursts: Automatically captures high-quality frames spread across the mathematical center of generated Shorts.<br />
<br />
Some feedback, advice, anything of the sort from a real human being outside myself and my streamer buddy who helped test it would be greatly appreciated thanks in advance to anyone who has the time to look at it.<br />
<br />
<a href="https://github.com/DeegoFronk/Auto-Vod-Trimmer" target="_blank" rel="noopener" class="mycode_url">Github</a><br />
<br />
My Current Road Map:<br />
<br />
 1. Docker / Linux / Mac Support: I built and deployed it strictly for Windows purely because that is the only hardware environment I currently have to test on. Wrapping it in Docker would be a much cleaner way to handle the FFmpeg dependencies and cross-platform pathing for Version 2.0.<br />
<br />
2. FFmpeg Split/Convert: Right now, the engine extracts the raw PCM float audio via an FFmpeg pipe to do the math, and then uses -c:v copy -c:a aac with a concat list for the final lossless render. Feeding it a non-standard codec or a weird .mkv wrapper will likely break that final concat step or cause audio drift.<br />
<br />
3. Benchmarks: I completely overlooked hardware benchmarking. The script currently outputs a ton of diagnostic stats to the terminal (Total runtime, Dead air trimmed, Peak WPM, Average Pitch, Chaos Scores) to help me tune the scoring math, but I haven't officially documented the hardware footprint beyond watching task manager and my CPU temps. For context, processing a 4-hour 1080p/60fps VOD on a standard 8-core CPU with 16GB RAM currently takes about 18-24 minutes. I estimate a full 6-hour VOD scales to roughly 30-35 minutes.]]></content:encoded>
		</item>
		<item>
			<title><![CDATA[Archive]]></title>
			<link>https://python-forum.io/thread-46300.html</link>
			<pubDate>Wed, 13 May 2026 22:28:22 +0000</pubDate>
			<dc:creator><![CDATA[<a href="https://python-forum.io/member.php?action=profile&uid=43866">OllieHa</a>]]></dc:creator>
			<guid isPermaLink="false">https://python-forum.io/thread-46300.html</guid>
			<description><![CDATA[Hi Guys,<br />
<br />
I want to make a website that stores files. <br />
<br />
Any help greatly appreciated.<br />
<br />
-Oliver]]></description>
			<content:encoded><![CDATA[Hi Guys,<br />
<br />
I want to make a website that stores files. <br />
<br />
Any help greatly appreciated.<br />
<br />
-Oliver]]></content:encoded>
		</item>
	</channel>
</rss>