From ffef599bf35bebd59d9bd9a9925d65d8f97abfed Mon Sep 17 00:00:00 2001 From: tcsenpai Date: Sun, 17 Nov 2024 19:29:04 +0100 Subject: [PATCH 1/5] added max_side_len to the ocr options --- memos/plugins/ocr/ppocr-gpu.yaml | 56 +++++++++++++++++--------------- memos/plugins/ocr/ppocr.yaml | 4 ++- 2 files changed, 32 insertions(+), 28 deletions(-) diff --git a/memos/plugins/ocr/ppocr-gpu.yaml b/memos/plugins/ocr/ppocr-gpu.yaml index 2a05d4b..31d8d71 100644 --- a/memos/plugins/ocr/ppocr-gpu.yaml +++ b/memos/plugins/ocr/ppocr-gpu.yaml @@ -1,41 +1,43 @@ Global: - text_score: 0.5 - use_det: true - use_cls: true - use_rec: true - print_verbose: false - min_height: 30 - width_height_ratio: 40 + text_score: 0.5 + use_det: true + use_cls: true + use_rec: true + print_verbose: false + min_height: 30 + width_height_ratio: 40 + max_side_len: 1500 + min_side_len: 30 Det: - use_cuda: true + use_cuda: true - model_path: models/ch_PP-OCRv4_det_infer.onnx + model_path: models/ch_PP-OCRv4_det_infer.onnx - limit_side_len: 1500 - limit_type: min + limit_side_len: 1500 + limit_type: min - thresh: 0.3 - box_thresh: 0.3 - max_candidates: 1000 - unclip_ratio: 1.6 - use_dilation: true - score_mode: fast + thresh: 0.3 + box_thresh: 0.3 + max_candidates: 1000 + unclip_ratio: 1.6 + use_dilation: true + score_mode: fast Cls: - use_cuda: true + use_cuda: true - model_path: models/ch_ppocr_mobile_v2.0_cls_train.onnx + model_path: models/ch_ppocr_mobile_v2.0_cls_train.onnx - cls_image_shape: [3, 48, 192] - cls_batch_num: 6 - cls_thresh: 0.9 - label_list: ['0', '180'] + cls_image_shape: [3, 48, 192] + cls_batch_num: 6 + cls_thresh: 0.9 + label_list: ["0", "180"] Rec: - use_cuda: true + use_cuda: true - model_path: models/ch_PP-OCRv4_rec_infer.onnx + model_path: models/ch_PP-OCRv4_rec_infer.onnx - rec_img_shape: [3, 48, 320] - rec_batch_num: 6 \ No newline at end of file + rec_img_shape: [3, 48, 320] + rec_batch_num: 6 diff --git a/memos/plugins/ocr/ppocr.yaml b/memos/plugins/ocr/ppocr.yaml index 3945096..be60e4e 100644 --- a/memos/plugins/ocr/ppocr.yaml +++ b/memos/plugins/ocr/ppocr.yaml @@ -7,6 +7,8 @@ Global: min_height: 30 width_height_ratio: 40 use_space_char: true + max_side_len: 1500 + min_side_len: 30 Det: use_cuda: false @@ -39,4 +41,4 @@ Rec: model_path: models/ch_PP-OCRv4_rec_infer.onnx rec_img_shape: [3, 48, 320] - rec_batch_num: 6 \ No newline at end of file + rec_batch_num: 6 From da8aff7e63972cdef2499231364a8e6144c05367 Mon Sep 17 00:00:00 2001 From: tcsenpai Date: Sun, 17 Nov 2024 19:29:17 +0100 Subject: [PATCH 2/5] added safe deps for linux --- linuxdeps.sh | 1 + 1 file changed, 1 insertion(+) create mode 100644 linuxdeps.sh diff --git a/linuxdeps.sh b/linuxdeps.sh new file mode 100644 index 0000000..56fb2d2 --- /dev/null +++ b/linuxdeps.sh @@ -0,0 +1 @@ +sudo apt install -y dbus-python python-xlib slurp grim maim spectacle \ No newline at end of file From fe5f754c23ca50774180b4dbc8af0e7bc5414dc5 Mon Sep 17 00:00:00 2001 From: tcsenpai Date: Sun, 17 Nov 2024 19:30:27 +0100 Subject: [PATCH 3/5] added linux support --- .gitignore | 5 + memos/cmds/library.py | 33 ++++- memos/commands.py | 280 +++++++++++++++++++++++++--------------- memos/record.py | 288 +++++++++++++++++++++++++++++++++++++++++- 4 files changed, 494 insertions(+), 112 deletions(-) diff --git a/.gitignore b/.gitignore index b08b64e..af0da0d 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,8 @@ test-data/ memos/static/ db/ memos/plugins/ocr/temp_ppocr.yaml +memos.spec +memosexec +screenshots +screenshots/ +yarn.lock diff --git a/memos/cmds/library.py b/memos/cmds/library.py index dc46759..2a1c591 100644 --- a/memos/cmds/library.py +++ b/memos/cmds/library.py @@ -15,6 +15,8 @@ from functools import lru_cache from collections import defaultdict, deque # Third-party imports +import platform +import subprocess import typer import httpx from tqdm import tqdm @@ -839,12 +841,31 @@ def sync( @lru_cache(maxsize=1) def is_on_battery(): - try: - battery = psutil.sensors_battery() - return battery is not None and not battery.power_plugged - except: - return False # If unable to detect battery status, assume not on battery - + + if platform.system() == "Darwin": + try: + result = subprocess.check_output(['pmset', '-g', 'batt']).decode() + return "'Battery Power'" in result + except: + return False + elif platform.system() == "Windows": + try: + return psutil.sensors_battery().power_plugged == False + except: + return False + elif platform.system() == "Linux": + try: + # Try using upower + result = subprocess.check_output(['upower', '--show-info', '/org/freedesktop/UPower/devices/battery_BAT0']).decode() + return 'state: discharging' in result.lower() + except: + try: + # Fallback to checking /sys/class/power_supply + with open('/sys/class/power_supply/BAT0/status', 'r') as f: + return f.read().strip().lower() == 'discharging' + except: + return False + return False # Modify the LibraryFileHandler class class LibraryFileHandler(FileSystemEventHandler): diff --git a/memos/commands.py b/memos/commands.py index bdcf41a..1d521f3 100644 --- a/memos/commands.py +++ b/memos/commands.py @@ -450,30 +450,43 @@ def remove_windows_autostart(): return False -@app.command() -def disable(): - """Disable memos from running at startup""" - if is_windows(): - if remove_windows_autostart(): - typer.echo( - "Removed Memos shortcut from startup folder. Memos will no longer run at startup." - ) - else: - typer.echo( - "Memos shortcut not found in startup folder. Memos is not set to run at startup." - ) - elif is_macos(): - plist_path = Path.home() / "Library/LaunchAgents/com.user.memos.plist" - if plist_path.exists(): - subprocess.run(["launchctl", "unload", str(plist_path)], check=True) - plist_path.unlink() - typer.echo( - "Unloaded and removed plist file. Memos will no longer run at startup." - ) - else: - typer.echo("Plist file does not exist. Memos is not set to run at startup.") - else: - typer.echo("Unsupported operating system.") +def generate_systemd_service(): + """Generate systemd service file for Linux.""" + memos_dir = settings.resolved_base_dir + python_path = get_python_path() + log_dir = memos_dir / "logs" + log_dir.mkdir(parents=True, exist_ok=True) + + service_content = f"""[Unit] +Description=Memos Service +After=network.target + +[Service] +Type=simple +Environment="PATH={os.environ['PATH']}" +ExecStart={python_path} -m memos.commands record +ExecStart={python_path} -m memos.commands serve +ExecStartPre=/bin/sleep 15 +ExecStart={python_path} -m memos.commands watch +Restart=always +User={os.getenv('USER')} +StandardOutput=append:{log_dir}/memos.log +StandardError=append:{log_dir}/memos.error.log + +[Install] +WantedBy=default.target +""" + + service_path = Path.home() / ".config/systemd/user" + service_path.mkdir(parents=True, exist_ok=True) + service_file = service_path / "memos.service" + with open(service_file, "w") as f: + f.write(service_content) + return service_file + + +def is_linux(): + return platform.system() == "Linux" @app.command() @@ -497,9 +510,145 @@ def enable(): plist_path = generate_plist() typer.echo(f"Generated plist file at {plist_path}") load_plist(plist_path) - typer.echo( - "Loaded plist file. Memos is started and will run at next startup or when 'start' command is used." - ) + typer.echo("Loaded plist file. Memos will run at next startup.") + elif is_linux(): + service_file = generate_systemd_service() + typer.echo(f"Generated systemd service file at {service_file}") + # Enable and start the service + subprocess.run(["systemctl", "--user", "enable", "memos.service"], check=True) + typer.echo("Enabled memos systemd service for current user.") + else: + typer.echo("Unsupported operating system.") + + +@app.command() +def disable(): + """Disable memos from running at startup""" + if is_windows(): + if remove_windows_autostart(): + typer.echo("Removed Memos shortcut from startup folder.") + else: + typer.echo("Memos shortcut not found in startup folder.") + elif is_macos(): + plist_path = Path.home() / "Library/LaunchAgents/com.user.memos.plist" + if plist_path.exists(): + subprocess.run(["launchctl", "unload", str(plist_path)], check=True) + plist_path.unlink() + typer.echo("Unloaded and removed plist file.") + else: + typer.echo("Plist file does not exist.") + elif is_linux(): + service_file = Path.home() / ".config/systemd/user/memos.service" + if service_file.exists(): + subprocess.run( + ["systemctl", "--user", "disable", "memos.service"], check=True + ) + subprocess.run(["systemctl", "--user", "stop", "memos.service"], check=True) + service_file.unlink() + typer.echo("Disabled and removed memos systemd service.") + else: + typer.echo("Systemd service file does not exist.") + else: + typer.echo("Unsupported operating system.") + + +@app.command() +def start(): + """Start all Memos processes""" + memos_dir = settings.resolved_base_dir + + if is_windows(): + bat_path = memos_dir / "launch.bat" + if not bat_path.exists(): + typer.echo("Launch script not found. Please run 'memos enable' first.") + return + try: + subprocess.Popen( + [str(bat_path)], shell=True, creationflags=subprocess.CREATE_NEW_CONSOLE + ) + typer.echo("Started Memos processes.") + except Exception as e: + typer.echo(f"Failed to start Memos processes: {str(e)}") + elif is_macos(): + service_name = "com.user.memos" + subprocess.run(["launchctl", "start", service_name], check=True) + typer.echo("Started Memos processes.") + elif is_linux(): + try: + subprocess.run( + ["systemctl", "--user", "start", "memos.service"], check=True + ) + typer.echo("Started Memos processes.") + except subprocess.CalledProcessError as e: + typer.echo(f"Failed to start Memos processes: {str(e)}") + else: + typer.echo("Unsupported operating system.") + + +@app.command() +def stop(): + """Stop all running Memos processes""" + if is_windows(): + services = ["serve", "watch", "record"] + stopped = False + for service in services: + processes = [ + p + for p in psutil.process_iter(["pid", "name", "cmdline"]) + if "python" in p.info["name"].lower() + and p.info["cmdline"] is not None + and "memos.commands" in p.info["cmdline"] + and service in p.info["cmdline"] + ] + + for process in processes: + try: + os.kill(process.info["pid"], signal.SIGTERM) + typer.echo( + f"Stopped {service} process (PID: {process.info['pid']})" + ) + stopped = True + except ProcessLookupError: + typer.echo( + f"Process {service} (PID: {process.info['pid']}) not found" + ) + except PermissionError: + typer.echo( + f"Permission denied to stop {service} process (PID: {process.info['pid']})" + ) + + if not stopped: + typer.echo("No running Memos processes found") + elif is_macos(): + service_name = "com.user.memos" + try: + subprocess.run(["launchctl", "stop", service_name], check=True) + typer.echo("Stopped Memos processes.") + except subprocess.CalledProcessError: + typer.echo("Failed to stop Memos processes. They may not be running.") + elif is_linux(): + try: + subprocess.run(["systemctl", "--user", "stop", "memos.service"], check=True) + typer.echo("Stopped Memos processes.") + except subprocess.CalledProcessError: + # Fallback to manual process killing if systemd service fails + services = ["serve", "watch", "record"] + stopped = False + for service in services: + try: + output = subprocess.check_output( + ["pgrep", "-f", f"memos.commands {service}"] + ) + pids = output.decode().strip().split() + for pid in pids: + os.kill(int(pid), signal.SIGTERM) + typer.echo(f"Stopped {service} process (PID: {pid})") + stopped = True + except (subprocess.CalledProcessError, ProcessLookupError): + continue + + if not stopped: + typer.echo("No running Memos processes found") else: typer.echo("Unsupported operating system.") @@ -538,83 +687,6 @@ def ps(): typer.echo(tabulate(table_data, headers=headers, tablefmt="plain")) -@app.command() -def stop(): - """Stop all running Memos processes""" - if is_windows(): - services = ["serve", "watch", "record"] - stopped = False - - for service in services: - processes = [ - p - for p in psutil.process_iter(["pid", "name", "cmdline"]) - if "python" in p.info["name"].lower() - and p.info["cmdline"] is not None - and "memos.commands" in p.info["cmdline"] - and service in p.info["cmdline"] - ] - - for process in processes: - try: - os.kill(process.info["pid"], signal.SIGTERM) - typer.echo( - f"Stopped {service} process (PID: {process.info['pid']})" - ) - stopped = True - except ProcessLookupError: - typer.echo( - f"Process {service} (PID: {process.info['pid']}) not found" - ) - except PermissionError: - typer.echo( - f"Permission denied to stop {service} process (PID: {process.info['pid']})" - ) - - if not stopped: - typer.echo("No running Memos processes found") - else: - typer.echo("All Memos processes have been stopped") - - elif is_macos(): - service_name = "com.user.memos" - try: - subprocess.run(["launchctl", "stop", service_name], check=True) - typer.echo("Stopped Memos processes.") - except subprocess.CalledProcessError: - typer.echo("Failed to stop Memos processes. They may not be running.") - - else: - typer.echo("Unsupported operating system.") - - -@app.command() -def start(): - """Start all Memos processes""" - memos_dir = settings.resolved_base_dir - - if is_windows(): - bat_path = memos_dir / "launch.bat" - if not bat_path.exists(): - typer.echo("Launch script not found. Please run 'memos enable' first.") - return - - try: - subprocess.Popen( - [str(bat_path)], shell=True, creationflags=subprocess.CREATE_NEW_CONSOLE - ) - typer.echo("Started Memos processes. Check the logs for more information.") - except Exception as e: - typer.echo(f"Failed to start Memos processes: {str(e)}") - - elif is_macos(): - service_name = "com.user.memos" - subprocess.run(["launchctl", "start", service_name], check=True) - typer.echo("Started Memos processes.") - else: - typer.echo("Unsupported operating system.") - - @app.command() def config(): """Show current configuration settings""" diff --git a/memos/record.py b/memos/record.py index fbc6b00..5764938 100644 --- a/memos/record.py +++ b/memos/record.py @@ -66,6 +66,64 @@ def save_previous_hashes(base_dir, previous_hashes): json.dump(previous_hashes, f) +def get_wayland_displays(): + displays = [] + try: + # Try using swaymsg for sway + output = subprocess.check_output(["swaymsg", "-t", "get_outputs"], text=True) + outputs = json.loads(output) + for output in outputs: + if output["active"]: + displays.append( + { + "name": output["name"], + "geometry": f"{output['rect'].x},{output['rect'].y} {output['rect'].width}x{output['rect'].height}", + } + ) + except: + try: + # Try using wlr-randr for wlroots-based compositors + output = subprocess.check_output(["wlr-randr"], text=True) + # Parse wlr-randr output + current_display = {} + for line in output.splitlines(): + if line.startswith(" "): + if "enabled" in line and "yes" in line: + current_display["active"] = True + else: + if current_display and current_display.get("active"): + displays.append(current_display) + current_display = {"name": line.split()[0]} + except: + # Fallback to single display + displays.append({"name": "", "geometry": ""}) + + return displays + + +def get_x11_displays(): + displays = [] + try: + output = subprocess.check_output(["xrandr", "--current"], text=True) + current_display = None + + for line in output.splitlines(): + if " connected " in line: + parts = line.split() + name = parts[0] + # Find the geometry in format: 1920x1080+0+0 + for part in parts: + if "x" in part and "+" in part: + geometry = part + break + displays.append({"name": name, "geometry": geometry}) + except: + # Fallback to single display + displays.append({"name": "default", "geometry": ""}) + + return displays + + def get_active_window_info_darwin(): active_app = NSWorkspace.sharedWorkspace().activeApplication() app_name = active_app["NSApplicationName"] @@ -94,11 +152,68 @@ def get_active_window_info_windows(): return "", "" +def get_active_window_info_linux(): + try: + # Try using xdotool for X11 + window_id = ( + subprocess.check_output(["xdotool", "getactivewindow"]).decode().strip() + ) + window_name = ( + subprocess.check_output(["xdotool", "getwindowname", window_id]) + .decode() + .strip() + ) + window_pid = ( + subprocess.check_output(["xdotool", "getwindowpid", window_id]) + .decode() + .strip() + ) + + app_name = "" + try: + with open(f"/proc/{window_pid}/comm", "r") as f: + app_name = f.read().strip() + except: + app_name = window_name.split(" - ")[0] + + return app_name, window_name + except: + try: + # Try using qdbus for Wayland/KDE + active_window = ( + subprocess.check_output( + ["qdbus", "org.kde.KWin", "/KWin", "org.kde.KWin.activeWindow"] + ) + .decode() + .strip() + ) + + window_title = ( + subprocess.check_output( + [ + "qdbus", + "org.kde.KWin", + f"/windows/{active_window}", + "org.kde.KWin.caption", + ] + ) + .decode() + .strip() + ) + + return window_title.split(" - ")[0], window_title + except: + return "", "" + + def get_active_window_info(): if platform.system() == "Darwin": return get_active_window_info_darwin() elif platform.system() == "Windows": return get_active_window_info_windows() + elif platform.system() == "Linux": + return get_active_window_info_linux() + return "", "" def take_screenshot_macos( @@ -231,13 +346,144 @@ def take_screenshot_windows( yield safe_monitor_name, webp_filename, "Saved" +def take_screenshot_linux( + base_dir, + previous_hashes, + threshold, + screen_sequences, + date, + timestamp, + app_name, + window_title, +): + screenshots = [] + + # Check if running under Wayland or X11 + wayland_display = os.environ.get("WAYLAND_DISPLAY") + is_wayland = wayland_display is not None + + if is_wayland: + # Try different Wayland screenshot tools in order of preference + screenshot_tools = [ + ["spectacle", "-m", "-b", "-n"], # Plasma default + ["grim"], # Basic Wayland screenshot utility + ["grimshot", "save"], # sway's screenshot utility + ["slurp", "-f", "%o"], # Alternative selection tool + ] + + for tool in screenshot_tools: + try: + # subprocess.run(["which", tool[0]], check=True, capture_output=True) + subprocess.run(["which", tool[0]], check=True, capture_output=True) + screenshot_cmd = tool + print(screenshot_cmd) + break + except subprocess.CalledProcessError: + continue + else: + raise RuntimeError( + "No supported Wayland screenshot tool found. Please install grim or grimshot." + ) + + else: + # X11 screenshot tools + screenshot_tools = [ + ["maim"], # Modern screenshot tool + ["scrot", "-z"], # Traditional screenshot tool + ["import", "-window", "root"], # ImageMagick + ] + + for tool in screenshot_tools: + try: + subprocess.run(["which", tool[0]], check=True, capture_output=True) + screenshot_cmd = tool + break + except subprocess.CalledProcessError: + continue + else: + raise RuntimeError( + "No supported X11 screenshot tool found. Please install maim, scrot, or imagemagick." + ) + + # Get display information using xrandr or Wayland equivalent + if is_wayland: + displays = get_wayland_displays() + else: + displays = get_x11_displays() + + for display_index, display_info in enumerate(displays): + screen_name = f"screen_{display_index}" + + temp_filename = os.path.join( + base_dir, date, f"temp_screenshot-{timestamp}-of-{screen_name}.png" + ) + + if is_wayland: + # For Wayland, we need to specify the output + output_arg = display_info["name"] + if output_arg == "": + output_arg = "0" + cmd = screenshot_cmd + ["-o", temp_filename] + print(cmd) + else: + # For X11, we can specify the geometry + geometry = display_info["geometry"] + cmd = screenshot_cmd + ["-g", geometry, temp_filename] + + try: + subprocess.run(cmd, check=True) + except subprocess.CalledProcessError as e: + logging.error(f"Failed to capture screenshot: {e}") + yield screen_name, None, "Failed to capture" + continue + + with Image.open(temp_filename) as img: + img = img.convert("RGB") + current_hash = str(imagehash.phash(img)) + + if ( + screen_name in previous_hashes + and imagehash.hex_to_hash(current_hash) + - imagehash.hex_to_hash(previous_hashes[screen_name]) + < threshold + ): + logging.info( + f"Screenshot for {screen_name} is similar to the previous one. Skipping." + ) + os.remove(temp_filename) + yield screen_name, None, "Skipped (similar to previous)" + continue + + previous_hashes[screen_name] = current_hash + screen_sequences[screen_name] = screen_sequences.get(screen_name, 0) + 1 + + metadata = { + "timestamp": timestamp, + "active_app": app_name, + "active_window": window_title, + "screen_name": screen_name, + "sequence": screen_sequences[screen_name], + } + + webp_filename = os.path.join( + base_dir, date, f"screenshot-{timestamp}-of-{screen_name}.webp" + ) + img.save(webp_filename, format="WebP", quality=85) + write_image_metadata(webp_filename, metadata) + + save_screen_sequences(base_dir, screen_sequences, date) + + os.remove(temp_filename) + yield screen_name, webp_filename, "Success" + + def take_screenshot( base_dir, previous_hashes, threshold, screen_sequences, date, timestamp ): app_name, window_title = get_active_window_info() + print(app_name, window_title) os.makedirs(os.path.join(base_dir, date), exist_ok=True) worklog_path = os.path.join(base_dir, date, "worklog") - with open(worklog_path, "a") as worklog: if platform.system() == "Darwin": screenshot_generator = take_screenshot_macos( @@ -261,6 +507,18 @@ def take_screenshot( app_name, window_title, ) + elif platform.system() == "Linux" or platform.system() == "linux": + print("Linux") + screenshot_generator = take_screenshot_linux( + base_dir, + previous_hashes, + threshold, + screen_sequences, + date, + timestamp, + app_name, + window_title, + ) else: raise NotImplementedError( f"Unsupported operating system: {platform.system()}" @@ -285,6 +543,29 @@ def is_screen_locked(): elif platform.system() == "Windows": user32 = ctypes.windll.User32 return user32.GetForegroundWindow() == 0 + elif platform.system() == "Linux": + try: + # Check for GNOME screensaver + output = subprocess.check_output( + ["gnome-screensaver-command", "-q"], stderr=subprocess.DEVNULL + ) + return b"is active" in output + except: + try: + # Check for XScreenSaver + output = subprocess.check_output( + ["xscreensaver-command", "-time"], stderr=subprocess.DEVNULL + ) + return b"screen locked" in output + except: + try: + # Check for Light-locker (XFCE, LXDE) + output = subprocess.check_output( + ["light-locker-command", "-q"], stderr=subprocess.DEVNULL + ) + return b"is locked" in output + except: + return False # If no screensaver utils found, assume not locked def run_screen_recorder_once(threshold, base_dir, previous_hashes): @@ -317,6 +598,7 @@ def run_screen_recorder(threshold, base_dir, previous_hashes): date, timestamp, ) + print(screenshot_files) for screenshot_file in screenshot_files: logging.info(f"Screenshot saved: {screenshot_file}") else: @@ -337,7 +619,9 @@ def main(): args = parser.parse_args() base_dir = ( - os.path.expanduser(args.base_dir) if args.base_dir else settings.resolved_screenshots_dir + os.path.expanduser(args.base_dir) + if args.base_dir + else settings.resolved_screenshots_dir ) previous_hashes = load_previous_hashes(base_dir) From 86dcd7992341b9495f3cec2d90e216b965b5fe03 Mon Sep 17 00:00:00 2001 From: tcsenpai Date: Sun, 17 Nov 2024 20:00:48 +0100 Subject: [PATCH 4/5] added quick setup & start for linux --- environment.yml | 55 +++++++++++++++++++++++++++++ linuxdeps.sh | 0 local_setup.sh | 24 +++++++++++++ start.py | 94 +++++++++++++++++++++++++++++++++++++++++++++++++ start.sh | 4 +++ 5 files changed, 177 insertions(+) create mode 100644 environment.yml mode change 100644 => 100755 linuxdeps.sh create mode 100755 local_setup.sh create mode 100644 start.py create mode 100755 start.sh diff --git a/environment.yml b/environment.yml new file mode 100644 index 0000000..0ee4bdf --- /dev/null +++ b/environment.yml @@ -0,0 +1,55 @@ +name: memos +channels: + - defaults +dependencies: + - _libgcc_mutex=0.1=main + - _openmp_mutex=5.1=1_gnu + - bzip2=1.0.8=h5eee18b_6 + - ca-certificates=2024.9.24=h06a4308_0 + - ld_impl_linux-64=2.40=h12ee557_0 + - libffi=3.4.4=h6a678d5_1 + - libgcc-ng=11.2.0=h1234567_1 + - libgomp=11.2.0=h1234567_1 + - libstdcxx-ng=11.2.0=h1234567_1 + - libuuid=1.41.5=h5eee18b_0 + - ncurses=6.4=h6a678d5_0 + - openssl=3.0.15=h5eee18b_0 + - pip=24.2=py310h06a4308_0 + - python=3.10.15=he870216_1 + - readline=8.2=h5eee18b_0 + - setuptools=75.1.0=py310h06a4308_0 + - sqlite=3.45.3=h5eee18b_0 + - tk=8.6.14=h39e8969_0 + - tzdata=2024b=h04d1e81_0 + - wheel=0.44.0=py310h06a4308_0 + - xz=5.4.6=h5eee18b_1 + - zlib=1.2.13=h5eee18b_1 + - pip: + - certifi==2024.8.30 + - colorama==0.4.6 + - coloredlogs==15.0.1 + - dbus-python==1.3.2 + - einops==0.8.0 + - fastapi==0.115.5 + - humanfriendly==10.0 + - imagehash==4.3.1 + - magika==0.5.1 + - memos==0.18.7 + - modelscope==1.20.1 + - mss==10.0.0 + - onnxruntime==1.20.0 + - piexif==1.1.3 + - psutil==6.1.0 + - py-cpuinfo==9.0.0 + - pydantic-settings==2.6.1 + - python-xlib==0.33 + - pywavelets==1.7.0 + - rapidocr-onnxruntime==1.3.25 + - shellingham==1.5.4 + - six==1.16.0 + - sqlite-vec==0.1.5 + - starlette==0.41.2 + - termcolor==2.5.0 + - timm==1.0.11 + - typer==0.13.0 + - uvicorn==0.32.0 diff --git a/linuxdeps.sh b/linuxdeps.sh old mode 100644 new mode 100755 diff --git a/local_setup.sh b/local_setup.sh new file mode 100755 index 0000000..4218536 --- /dev/null +++ b/local_setup.sh @@ -0,0 +1,24 @@ +#! /bin/bash + +# Build web app +cd web +yarn || exit 1 +yarn build || exit 1 +cd .. + +# Install linux dependencies +./linuxdeps.sh || exit 1 + +# Install python dependencies in conda environment +conda env create -f environment.yml || exit 1 + +# Activate conda environment +conda activate memos || exit 1 + +# Initialize database +python memos_app.py init || exit 1 + +# Deactivate and exit +conda deactivate +echo "Setup complete. Please run 'conda activate memos' to use the environment and then 'python start.py' to start the full app." +echo "You can also run 'source start.sh' to start the full app in one go." \ No newline at end of file diff --git a/start.py b/start.py new file mode 100644 index 0000000..93ab44f --- /dev/null +++ b/start.py @@ -0,0 +1,94 @@ +import subprocess +import threading +import sys +import signal +from colorama import init, Fore +import time + +# Initialize colorama for Windows compatibility +init() + +# Define colors for each process +COLORS = [Fore.GREEN, Fore.BLUE, Fore.YELLOW] + + +def run_process(command, color): + """Run a single process with colored output.""" + try: + process = subprocess.Popen( + command, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + bufsize=1, + universal_newlines=True, + ) + + while True: + line = process.stdout.readline() + if not line and process.poll() is not None: + break + if line: + print(f"{color}{command[0]}: {line.rstrip()}{Fore.RESET}") + + return process.poll() + + except Exception as e: + print(f"{Fore.RED}Error in {command[0]}: {str(e)}{Fore.RESET}") + return 1 + + +def main(): + # Define your three commands here + commands = [ + ["python", "memos_app.py", "record"], + ["python", "memos_app.py", "serve"], + ["python", "memos_app.py", "watch"], + ] + + # Create threads for each process + threads = [] + processes = [] + + def signal_handler(signum, frame): + print(f"\n{Fore.RED}Interrupting all processes...{Fore.RESET}") + for process in processes: + process.terminate() + sys.exit(0) + + # Set up signal handlers + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + + # Run processes in separate threads + for i, command in enumerate(commands): + time.sleep(3) + color = COLORS[i % len(COLORS)] + process = subprocess.Popen( + command, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + bufsize=1, + universal_newlines=True, + ) + processes.append(process) + thread = threading.Thread(target=run_process, args=(command, color)) + thread.start() + threads.append(thread) + print(f"Started {command[0]} with PID {process.pid}") + + # Wait for all threads to complete + for thread in threads: + thread.join() + + # Check for any failed processes + failed_processes = [process for process in processes if process != 0] + if failed_processes: + print(f"\n{Fore.RED}Some processes failed: {failed_processes}{Fore.RESET}") + else: + print(f"\n{Fore.GREEN}All processes completed successfully!{Fore.RESET}") + + +if __name__ == "__main__": + main() diff --git a/start.sh b/start.sh new file mode 100755 index 0000000..50ad430 --- /dev/null +++ b/start.sh @@ -0,0 +1,4 @@ +#! /usr/bin/env bash + +conda activate memos || exit 1 +python start.py \ No newline at end of file From e9e83306c1261eca2fda400746250a42fa8d6dba Mon Sep 17 00:00:00 2001 From: tcsenpai Date: Sun, 17 Nov 2024 20:07:27 +0100 Subject: [PATCH 5/5] updated instructions --- README.md | 54 +++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 41 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index f5327f4..c8ee4a3 100644 --- a/README.md +++ b/README.md @@ -26,15 +26,17 @@ This project draws heavily from two other projects: one called [Rewind](https:// ## Quick Start +### MacOS & Windows + ![memos-installation](docs/images/memos-installation.gif) -### 1. Install Pensieve +#### 1. Install Pensieve ```sh pip install memos ``` -### 2. Initialize +#### 2. Initialize Initialize the pensieve configuration file and sqlite database: @@ -44,7 +46,7 @@ memos init Data will be stored in the `~/.memos` directory. -### 3. Start the Service +#### 3. Start the Service ```sh memos enable @@ -57,6 +59,32 @@ This command will: - Start the Web service - Set the service to start on boot +### Linux + +**Note:** Linux support is still under development. At the moment, you can run the app by following the steps below. + +**Important:** You need to have `conda` installed to run the app. Also, if something is not working, check the single commands in the shell files to see if they are working. + +- [x] Tested on Ubuntu 22.04 + KDE Plasma + Wayland + +#### 1. Install Dependencies + +```sh +./linuxdeps.sh +``` + +#### 2. Install Pensieve + +```sh +./local_setup.sh +``` + +#### 3. Start the App + +```sh +source start.sh +``` + ### 4. Access the Web Interface Open your browser and visit `http://localhost:8839` @@ -88,9 +116,9 @@ Open the `~/.memos/config.yaml` file with your preferred text editor and modify embedding: enabled: true use_local: true - model: jinaai/jina-embeddings-v2-base-en # Model name used - num_dim: 768 # Model dimensions - use_modelscope: false # Whether to use ModelScope's model + model: jinaai/jina-embeddings-v2-base-en # Model name used + num_dim: 768 # Model dimensions + use_modelscope: false # Whether to use ModelScope's model ``` #### 3. Restart Memos Service @@ -153,11 +181,11 @@ Open the `~/.memos/config.yaml` file with your preferred text editor and modify ```yaml vlm: - enabled: true # Enable VLM feature - endpoint: http://localhost:11434 # Ollama service address - modelname: minicpm-v # Model name to use - force_jpeg: true # Convert images to JPEG format to ensure compatibility - prompt: Please describe the content of this image, including the layout and visual elements # Prompt sent to the model + enabled: true # Enable VLM feature + endpoint: http://localhost:11434 # Ollama service address + modelname: minicpm-v # Model name to use + force_jpeg: true # Convert images to JPEG format to ensure compatibility + prompt: Please describe the content of this image, including the layout and visual elements # Prompt sent to the model ``` Use the above configuration to overwrite the `vlm` configuration in the `~/.memos/config.yaml` file. @@ -166,8 +194,8 @@ Also, modify the `default_plugins` configuration in the `~/.memos/plugins/vlm/co ```yaml default_plugins: -- builtin_ocr -- builtin_vlm + - builtin_ocr + - builtin_vlm ``` This adds the `builtin_vlm` plugin to the default plugin list.