diff --git a/.bash_aliases b/.bash_aliases index 302e6b8..9f68795 100644 --- a/.bash_aliases +++ b/.bash_aliases @@ -127,4 +127,5 @@ alias hypr='cd ~/.config/hypr && vim ~/.config/hypr/hyprland.conf && cd -' alias wlc='wl-copy' alias wlp='wl-paste' alias vn32='WINEPREFIX=/home/sudacode/S/lutris/wineprefix32 WINEARCH=win32' -alias impv='mpv --profile=immersion' +alias impv='mpv --profile=subminer' +alias smpv='mpv --profile=subminer' diff --git a/.claude/settings.json##os.Linux b/.claude/settings.json##os.Linux index 2b8d257..ad747a9 100644 --- a/.claude/settings.json##os.Linux +++ b/.claude/settings.json##os.Linux @@ -39,19 +39,23 @@ "pyright-lsp@claude-plugins-official": true, "typescript-lsp@claude-plugins-official": true, "clangd-lsp@claude-plugins-official": true, - "claude-mem@thedotmack": true, "frontend-design@claude-plugins-official": true, "code-review@claude-plugins-official": true, "code-simplifier@claude-plugins-official": true, - "playwright@claude-plugins-official": true + "playwright@claude-plugins-official": true, + "superpowers@claude-plugins-official": true }, "sandbox": { "enabled": false, "autoAllowBashIfSandboxed": true, "network": { - "allowUnixSockets": ["/var/run/docker.sock"], + "allowUnixSockets": [ + "/var/run/docker.sock" + ], "allowLocalBinding": true }, - "excludedCommands": ["docker"] + "excludedCommands": [ + "docker" + ] } } diff --git a/.config/SubMiner/config.jsonc##os.Darwin b/.config/SubMiner/config.jsonc##os.Darwin new file mode 100755 index 0000000..e2bbeb9 --- /dev/null +++ b/.config/SubMiner/config.jsonc##os.Darwin @@ -0,0 +1,131 @@ +{ + "keybindings": [], + "shortcuts": { + "copySubtitle": "CommandOrControl+C", + "copySubtitleMultiple": "CommandOrControl+Shift+C", + "updateLastCardFromClipboard": "CommandOrControl+V", + "triggerFieldGrouping": "CommandOrControl+G", + "triggerSubsync": "CommandOrControl+Alt+S", + "mineSentence": "CommandOrControl+S", + "mineSentenceMultiple": "CommandOrControl+Shift+S", + "multiCopyTimeoutMs": 3000, + "toggleSecondarySub": "CommandOrControl+Shift+V", + "markAudioCard": "CommandOrControl+Shift+A", + "openRuntimeOptions": "CommandOrControl+Shift+O", + "toggleVisibleOverlayGlobal": "Alt+Shift+O", + "toggleInvisibleOverlayGlobal": "Alt+Shift+I", + }, + "auto_start_overlay": false, + "bind_visible_overlay_to_mpv_sub_visibility": false, + "texthooker": { + "openBrowser": false, + }, + "websocket": { + "enabled": "auto", + "port": 6677, + }, + "ankiConnect": { + "enabled": true, + "url": "http://127.0.0.1:8765", + "deck": "Minecraft", + "pollingRate": 500, + "fields": { + "audio": "ExpressionAudio", + "image": "Picture", + "sentence": "Sentence", + "miscInfo": "MiscInfo", + "translation": "SelectionText", + }, + "openRouter": { + "enabled": true, + "alwaysUseAiTranslation": true, + "apiKey": "", + "model": "openai/gpt-oss-120b:free", + "baseUrl": "https://openrouter.ai/api/v1", + "sourceLanguage": "Japanese", + "systemPrompt": "You are a translation engine for translating Japanese into natural-sounding, context-aware English. Return only the translated text with no extra explanations or commentary. The translation must preserve the original tone and intent of the source. If the input is not in the target language, translate it to the target language. If the input is already in the target language, return it as is.", + }, + "media": { + "generateAudio": true, + "generateImage": true, + "imageType": "avif", + "imageFormat": "webp", + "animatedFps": 24, + "animatedMaxWidth": 640, + "animatedMaxHeight": null, + "animatedCrf": 35, + "audioPadding": 0.5, + "fallbackDuration": 3, + }, + "behavior": { + "overwriteAudio": false, + "overwriteImage": true, + "mediaInsertMode": "append", + "highlightWord": true, + "notificationType": "system", + "showNotificationOnUpdate": true, + "autoUpdateNewCards": false, + }, + "nPlusOne": { + "decks": ["Minecraft", "Kaishi 1.5k"], + "highlightEnabled": true, + "refreshMinutes": 60, + "matchMode": "headword", + }, + "metadata": { + "pattern": "[SubMiner] %f (%t)", + }, + "isLapis": { + "enabled": true, + "sentenceCardModel": "Lapis Morph", + "sentenceCardSentenceField": "Sentence", + "sentenceCardAudioField": "SentenceAudio", + }, + "isKiku": { + "enabled": true, + "fieldGrouping": "manual", + "deleteDuplicateInAuto": true, + }, + }, + "secondarySub": { + "autoLoadSecondarySub": true, + "secondarySubLanguages": ["en", "eng"], + }, + "subsync": { + "defaultMode": "manual", + "alass_path": "~/.local/bin/alass-cli", + "ffsubsync_path": "~/.local/bin/ffsubsync", + "ffmpeg_path": "/opt/homebrew/ffmpeg/bin/ffmpeg", + }, + "subtitleStyle": { + "fontFamily": "Noto Sans CJK JP Regular, Noto Sans CJK JP, Arial Unicode MS, Arial, sans-serif", + "fontSize": 35, + "fontColor": "#cad3f5", + "fontWeight": "normal", + "fontStyle": "normal", + "backgroundColor": "rgb(30, 32, 48, 0.88)", + "secondary": { + "fontSize": 24, + "fontColor": "#cad3f5", + "backgroundColor": "transparent", + }, + }, + "jimaku": { + // "apiKey": "YOUR_API_KEY", + // or use a command that outputs the key: + "apiKeyCommand": "cat ~/.jimaku-api-key", + "apiBaseUrl": "https://jimaku.cc", + "languagePreference": "ja", + "maxEntryResults": 10, + }, + "invisibleOverlay": { + // "platform-default" => hidden on Wayland, visible elsewhere + // other values: "visible", "hidden" + "startupVisibility": "platform-default", + }, + "youtubeSubgen": { + "mode": "automatic", // automatic | preprocess | off + "whisperBin": "/usr/bin/whisper-cli", + "whisperModel": "~/models/whisper.cpp/ggml-small.bin", + }, +} diff --git a/.config/SubMiner/config.jsonc##os.Linux b/.config/SubMiner/config.jsonc##os.Linux new file mode 100644 index 0000000..316c965 --- /dev/null +++ b/.config/SubMiner/config.jsonc##os.Linux @@ -0,0 +1,131 @@ +{ + "keybindings": [], + "shortcuts": { + "copySubtitle": "CommandOrControl+C", + "copySubtitleMultiple": "CommandOrControl+Shift+C", + "updateLastCardFromClipboard": "CommandOrControl+V", + "triggerFieldGrouping": "CommandOrControl+G", + "triggerSubsync": "CommandOrControl+Alt+S", + "mineSentence": "CommandOrControl+S", + "mineSentenceMultiple": "CommandOrControl+Shift+S", + "multiCopyTimeoutMs": 3000, + "toggleSecondarySub": "CommandOrControl+Shift+V", + "markAudioCard": "CommandOrControl+Shift+A", + "openRuntimeOptions": "CommandOrControl+Shift+O", + "toggleVisibleOverlayGlobal": "Alt+Shift+O", + "toggleInvisibleOverlayGlobal": "Alt+Shift+I", + }, + "auto_start_overlay": false, + "bind_visible_overlay_to_mpv_sub_visibility": false, + "texthooker": { + "openBrowser": false, + }, + "websocket": { + "enabled": "auto", + "port": 6677, + }, + "ankiConnect": { + "enabled": true, + "url": "http://127.0.0.1:8765", + "deck": "Minecraft", + "pollingRate": 500, + "fields": { + "audio": "ExpressionAudio", + "image": "Picture", + "sentence": "Sentence", + "miscInfo": "MiscInfo", + "translation": "SelectionText", + }, + "openRouter": { + "enabled": true, + "alwaysUseAiTranslation": true, + "apiKey": "", + "model": "openai/gpt-oss-120b:free", + "baseUrl": "https://openrouter.ai/api/v1", + "sourceLanguage": "Japanese", + "systemPrompt": "You are a translation engine for translating Japanese into natural-sounding, context-aware English. Return only the translated text with no extra explanations or commentary. The translation must preserve the original tone and intent of the source. If the input is not in the target language, translate it to the target language. If the input is already in the target language, return it as is.", + }, + "media": { + "generateAudio": true, + "generateImage": true, + "imageType": "avif", + "imageFormat": "webp", + "animatedFps": 24, + "animatedMaxWidth": 640, + "animatedMaxHeight": null, + "animatedCrf": 35, + "audioPadding": 0.5, + "fallbackDuration": 3, + }, + "behavior": { + "overwriteAudio": false, + "overwriteImage": true, + "mediaInsertMode": "append", + "highlightWord": true, + "notificationType": "system", + "showNotificationOnUpdate": true, + "autoUpdateNewCards": false, + }, + "nPlusOne": { + "decks": ["Minecraft", "Kaishi 1.5k"], + "highlightEnabled": true, + "refreshMinutes": 60, + "matchMode": "headword", + }, + "metadata": { + "pattern": "[SubMiner] %f (%t)", + }, + "isLapis": { + "enabled": true, + "sentenceCardModel": "Lapis Morph", + "sentenceCardSentenceField": "Sentence", + "sentenceCardAudioField": "SentenceAudio", + }, + "isKiku": { + "enabled": true, + "fieldGrouping": "manual", + "deleteDuplicateInAuto": true, + }, + }, + "secondarySub": { + "autoLoadSecondarySub": true, + "secondarySubLanguages": ["en", "eng"], + }, + "subsync": { + "defaultMode": "manual", + "alass_path": null, + "ffsubsync_path": null, + "ffmpeg_path": null, + }, + "subtitleStyle": { + "fontFamily": "Noto Sans CJK JP Regular, Noto Sans CJK JP, Arial Unicode MS, Arial, sans-serif", + "fontSize": 35, + "fontColor": "#cad3f5", + "fontWeight": "normal", + "fontStyle": "normal", + "backgroundColor": "rgb(30, 32, 48, 0.88)", + "secondary": { + "fontSize": 24, + "fontColor": "#cad3f5", + "backgroundColor": "transparent", + }, + }, + "jimaku": { + // "apiKey": "YOUR_API_KEY", + // or use a command that outputs the key: + "apiKeyCommand": "cat ~/.jimaku-api-key", + "apiBaseUrl": "https://jimaku.cc", + "languagePreference": "ja", + "maxEntryResults": 10, + }, + "invisibleOverlay": { + // "platform-default" => hidden on Wayland, visible elsewhere + // other values: "visible", "hidden" + "startupVisibility": "platform-default", + }, + "youtubeSubgen": { + "mode": "automatic", // automatic | preprocess | off + "whisperBin": "/usr/bin/whisper-cli", + "whisperModel": "~/models/whisper.cpp/ggml-small.bin", + }, +} diff --git a/.config/hypr/hyprland.conf b/.config/hypr/hyprland.conf index 6ef8cf0..a7807bb 100644 --- a/.config/hypr/hyprland.conf +++ b/.config/hypr/hyprland.conf @@ -78,6 +78,7 @@ exec-once = uwsm app -sb -t service -- nm-applet exec-once = uwsm app -sb -t service -- waybar -c ~/.config/waybar/catppuccin-macchiato/config.jsonc -s ~/.config/waybar/catppuccin-macchiato/style.css exec-once = uwsm app -sb -t service -- hyprsunset exec-once = uwsm app -sb -t service -- /usr/lib/polkit-kde-authentication-agent-1 +exec-once = uwsm app -sb -t service -- gnome-keyring-daemon --start --components=secrets,ssh,pkcs11 # exec-once = uwsm app -sb -t service -- variety exec-once = ~/.local/bin/aria # exec-once = dbus-update-activation-environment --systemd WAYLAND_DISPLAY XDG_CURRENT_DESKTOP diff --git a/.config/hypr/keybindings.conf b/.config/hypr/keybindings.conf index 68e037a..c6f74b9 100644 --- a/.config/hypr/keybindings.conf +++ b/.config/hypr/keybindings.conf @@ -95,7 +95,7 @@ bind = $mainMod SHIFT, v, exec, uwsm app -sb -- rofi-rbw # bind = $mainMod, w, exec, rofi -show window -theme $HOME/.config/rofi/launchers/type-2/style-2.rasi -dpi 96 -theme-str 'window {width: 35%;}' bind = $mainMod SHIFT, w, exec, "$HOME/.config/rofi/scripts/rofi-wallpaper.sh" bind = $mainMod SHIFT, d, exec, "$HOME/.config/rofi/scripts/rofi-docs.sh" -bind = SUPER, j, exec, "$HOME/.config/rofi/scripts/rofi-jellyfin-dir.sh" +bind = SUPER SHIFT, j, exec, "$HOME/.config/rofi/scripts/rofi-jellyfin-dir.sh" bind = SUPER, t, exec, "$HOME/.config/rofi/scripts/rofi-launch-texthooker-steam.sh" bind = $mainMod SHIFT, t, exec, "$HOME/projects/scripts/popup-ai-translator.py" bind = SUPER SHIFT, g, exec, "$HOME/.config/rofi/scripts/rofi-vn-helper.sh" @@ -133,7 +133,7 @@ bind = $mainMod, z, exec, uwsm app -sb -- zen-browser bind = $mainMod SHIFT, s, exec , rofi -show ssh -theme "$HOME/.config/rofi/launchers/type-2/style-2.rasi" -terminal -theme-str 'window{width: 25%;} listview {columns: 1; lines: 10;}' ghostty -ssh-command "ghostty --initial-command='TERM=kitty ssh {host}'" # reload monitors -bind = $mainMod SHIFT, R, exec, hyprctl dispatch dpms off && sleep 1 && hyprctl dispatch dpms on +bind = CTRL $mainMod SHIFT, R, exec, hyprctl dispatch dpms off && sleep 1 && hyprctl dispatch dpms on # Disable keybinds with one master keybind # https://wiki.hypr.land/0.49.0/Configuring/Uncommon-tips--tricks/#disabling-keybinds-with-one-master-keybind diff --git a/.config/mpv-modules/ani-skip b/.config/mpv-modules/ani-skip new file mode 160000 index 0000000..12b4960 --- /dev/null +++ b/.config/mpv-modules/ani-skip @@ -0,0 +1 @@ +Subproject commit 12b4960ecd34082a47ae4387d802b0a2847ec0a2 diff --git a/.config/tmux/tmux.conf b/.config/tmux/tmux.conf index 0352340..36f292a 100644 --- a/.config/tmux/tmux.conf +++ b/.config/tmux/tmux.conf @@ -85,6 +85,8 @@ bind j select-pane -D bind k select-pane -U bind l select-pane -R +bind r source-file ~/.tmux.conf \; display "Reloaded tmux config" + set-window-option -g mode-keys vi set -g mode-keys vi set -g status-keys vi diff --git a/.gitmodules b/.gitmodules index 7003048..8385783 100644 --- a/.gitmodules +++ b/.gitmodules @@ -25,3 +25,6 @@ [submodule ".config/mpv-modules/animecards"] path = .config/mpv-modules/animecards url = git@github.com:ksyasuda/Anacreon-Script +[submodule ".config/mpv-modules/ani-skip"] + path = .config/mpv-modules/ani-skip + url = https://github.com/synacktraa/ani-skip.git diff --git a/.zsh/.zshrc##default b/.zsh/.zshrc##default index 01d7dee..b73332f 100644 --- a/.zsh/.zshrc##default +++ b/.zsh/.zshrc##default @@ -102,3 +102,11 @@ zle -N bracketed-paste bracketed-paste-magic alias claude-mem='bun "/home/sudacode/.claude/plugins/marketplaces/thedotmack/plugin/scripts/worker-service.cjs"' fpath=(/home/sudacode/.zsh/completions $fpath) autoload -Uz compinit && compinit + +# pnpm +export PNPM_HOME="/home/sudacode/.local/share/pnpm" +case ":$PATH:" in + *":$PNPM_HOME:"*) ;; + *) export PATH="$PNPM_HOME:$PATH" ;; +esac +# pnpm end diff --git a/projects/scripts/mpv-add.sh b/projects/scripts/mpv-add.sh index 9a9a760..bcf867d 100755 --- a/projects/scripts/mpv-add.sh +++ b/projects/scripts/mpv-add.sh @@ -2,28 +2,82 @@ set -Eeuo pipefail -URL="${1:-$(wl-paste -p)}" -MPV_SOCKET=/tmp/mpvsocket +URL="${1:-}" +MPV_SOCKET="/tmp/mpvsocket" ICON_PATH="$HOME/.local/share/icons/Magna-Glassy-Dark-Icons/apps/48/mpv.svg" TITLE="mpv-add.sh" +notify() { + local message="$1" + notify-send -i "$ICON_PATH" "$TITLE" "$message" +} + +trim() { + printf '%s' "$1" | sed 's/^[[:space:]]\+//;s/[[:space:]]\+$//' +} + +play_direct() { + mpv -- "$URL" &> /dev/null & +} + +wait_for_socket() { + local timeout_ms=2000 + local waited=0 + while ((waited < timeout_ms)); do + if [[ -S "$MPV_SOCKET" ]]; then + return 0 + fi + sleep 0.05 + waited=$((waited + 50)) + done + return 1 +} + +is_valid_input() { + local input="$1" + + if [[ -f "$input" ]]; then + return 0 + fi + + if ! command -v yt-dlp > /dev/null 2>&1; then + return 1 + fi + + if yt-dlp --simulate "$input" > /dev/null 2>&1; then + return 0 + fi + + return 1 +} + +if [[ -z "$URL" ]] && command -v wl-paste > /dev/null 2>&1; then + URL="$(wl-paste -p || true)" +fi + +URL="$(trim "$URL")" + if [[ -z "$URL" ]]; then - notify-send -i "$ICON_PATH" "$TITLE" "No URL provided" + notify "No URL provided" exit 1 fi -if ! [[ -f "$URL" ]] && ! yt-dlp --simulate "$URL"; then - notify-send -i "$ICON_PATH" "$TITLE" "Invalid URL" +if ! is_valid_input "$URL"; then + notify "Invalid input: provide a local file path or a yt-dlp supported URL" exit 1 fi if ! pgrep -x mpv &> /dev/null; then - mpv "$URL" &> /dev/null & - notify-send -i "$ICON_PATH" "$TITLE" "Playing $URL" + rm -f "$MPV_SOCKET" + mpv --input-ipc-server="$MPV_SOCKET" -- "$URL" &> /dev/null & + notify "Playing $URL" else - if echo "{ \"command\": [\"script-message\", \"add_to_queue\", \"$URL\" ] }" | socat - "$MPV_SOCKET" &> /dev/null; then - notify-send -i "$ICON_PATH" "$TITLE" "Added $URL to queue" - else - notify-send -i "$ICON_PATH" "$TITLE" "Failed to add $URL to queue" + if [[ -S "$MPV_SOCKET" ]] && wait_for_socket && echo "{ \"command\": [\"script-message\", \"add_to_queue\", \"$URL\" ] }" | socat - "$MPV_SOCKET" &> /dev/null; then + notify "Added $URL to queue" + exit 0 fi + + notify "Queue unavailable, opening in new player" + play_direct + notify "Playing $URL" fi diff --git a/projects/scripts/rmpv b/projects/scripts/rmpv index dddc8be..d91bd0f 100755 --- a/projects/scripts/rmpv +++ b/projects/scripts/rmpv @@ -4,6 +4,7 @@ THEME="${THEME:-$HOME/.local/share/SubMiner/themes/subminer.rasi}" FONTCONFIG_FILE=$HOME/.config/mpv/mpv-fonts.conf COMMAND=mpv VIDEO_EXTENSIONS="mkv|mp4|avi|webm|mov|flv|wmv|m4v|ts|m2ts" +TARGET_DIRECTORY="" # Parse command-line options first while getopts ":st:" opt; do @@ -26,6 +27,19 @@ while getopts ":st:" opt; do done shift $((OPTIND - 1)) +if [[ $# -gt 1 ]]; then + echo "Usage: $0 [-s] [-t theme] [directory]" >&2 + exit 1 +fi + +if [[ $# -eq 1 ]]; then + if [[ ! -d "$1" ]]; then + echo "Invalid directory: $1" >&2 + exit 1 + fi + TARGET_DIRECTORY="$(realpath "$1")" +fi + find_videos() { find "$PWD" -maxdepth 1 -type f -regextype posix-extended \ -iregex ".*\.($VIDEO_EXTENSIONS)$" 2> /dev/null | sort -V @@ -40,6 +54,60 @@ build_rofi_menu() { done < <(find_videos) } +is_video_file() { + local file="$1" + local regex="\\.($VIDEO_EXTENSIONS)$" + + if [[ ! "${file,,}" =~ $regex ]]; then + return 1 + fi + + if command -v file > /dev/null 2>&1; then + local mime + mime=$(file -b --mime-type "$file" 2>/dev/null || true) + [[ "$mime" == video/* ]] && return 0 + fi + + return 0 +} + +select_video_via_rofi() { + if [[ -n "$TARGET_DIRECTORY" ]]; then + local selection="" + local prompt="Choose Video " + while true; do + selection=$(rofi -show filebrowser -show-icons -theme "$THEME" \ + -theme-str 'configuration {font: "JetBrainsMono Nerd Font 10";} listview {columns: 1; lines: 15;} window {width: 88%;}' \ + -p "$prompt" \ + -filebrowser-directory "$TARGET_DIRECTORY" \ + -filebrowser-cancel-returns-1 true) + + [[ -z "$selection" ]] && return 1 + + if [[ "$selection" != /* ]]; then + selection="$TARGET_DIRECTORY/$selection" + fi + + if [[ -d "$selection" ]]; then + prompt="Choose Video (folders are not selectable)" + continue + fi + + if is_video_file "$selection"; then + echo "$selection" + return 0 + fi + + prompt="Choose Video (select a video file)" + done + fi + + build_rofi_menu | rofi -dmenu -i -show-icons -theme "$THEME" \ + -theme-str 'configuration {font: "JetBrainsMono Nerd Font 10";} listview {columns: 1; lines: 15;} window {width: 88%;}' -p "Choose Video " +} + +selection=$(select_video_via_rofi) + get_video_thumbnail() { local video="$1" local thumb_dir="$HOME/.cache/thumbnails/large" @@ -60,15 +128,24 @@ get_video_thumbnail() { fi } -selection=$(build_rofi_menu | rofi -dmenu -i -show-icons -theme "$THEME" \ - -theme-str 'configuration {font: "JetBrainsMono Nerd Font 10";} listview {columns: 1; lines: 15;} window {width: 88%;}' -p "Choose Video ") - if [[ -z "$selection" ]]; then echo "No video selected." exit 1 fi -choice="./$selection" +if [[ -n "$TARGET_DIRECTORY" ]]; then + choice="$selection" + if [[ "$choice" != /* ]]; then + choice="$TARGET_DIRECTORY/$choice" + fi +else + choice="./$selection" +fi + +if [[ ! -f "$choice" ]]; then + echo "Selected item is not a valid file: $choice" >&2 + exit 1 +fi THUMBNAIL_PATH=$(get_video_thumbnail "$choice") if [[ -n "$THUMBNAIL_PATH" && -f "$THUMBNAIL_PATH" ]]; then diff --git a/projects/scripts/whisper_record_transcribe.py b/projects/scripts/whisper_record_transcribe.py index 79e4e7f..38ba47a 100755 --- a/projects/scripts/whisper_record_transcribe.py +++ b/projects/scripts/whisper_record_transcribe.py @@ -444,6 +444,11 @@ def _type_with_tool(text: str) -> None: raise RuntimeError("No typing tool found. Install one of: wtype, ydotool, xdotool.") +def _normalize_text_for_typing(text: str) -> str: + # Prevent simulated Enter key presses from transcript line breaks. + return " ".join(text.split()) + + def _emit_text(text: str, args: argparse.Namespace, notifier: Notifier) -> int: if args.output == "print": print(text) @@ -451,7 +456,7 @@ def _emit_text(text: str, args: argparse.Namespace, notifier: Notifier) -> int: return 0 try: - _type_with_tool(text) + _type_with_tool(_normalize_text_for_typing(text)) except Exception as exc: print(f"Failed to simulate typing: {exc}", file=sys.stderr) notifier.send("Typing error", str(exc), timeout_ms=2500)