localcoder 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
localcoder/voice.py ADDED
@@ -0,0 +1,187 @@
1
+ """Voice input — hold Space to talk, release to transcribe."""
2
+ import os, subprocess, signal, tempfile, time, shutil
3
+ from pathlib import Path
4
+
5
+ from rich.console import Console
6
+
7
+ console = Console()
8
+
9
+ # ── Paths ──
10
+ WHISPER_BIN = shutil.which("whisper-cli") or "/opt/homebrew/bin/whisper-cli"
11
+ WHISPER_MODEL_DIR = Path.home() / ".local/share/whisper"
12
+ WHISPER_MODEL = WHISPER_MODEL_DIR / "ggml-base.bin"
13
+ SOX_REC = shutil.which("rec") or "/opt/homebrew/bin/rec"
14
+
15
+
16
+ def is_voice_available():
17
+ """Check if voice input dependencies are installed."""
18
+ return os.path.exists(WHISPER_BIN) and WHISPER_MODEL.exists() and os.path.exists(SOX_REC)
19
+
20
+
21
+ def check_mic_permission():
22
+ """Check macOS microphone permission by attempting a quick recording."""
23
+ try:
24
+ tmp = tempfile.mktemp(suffix=".wav")
25
+ proc = subprocess.run(
26
+ [SOX_REC, "-q", "-r", "16000", "-c", "1", "-b", "16", tmp, "trim", "0", "0.5"],
27
+ capture_output=True, text=True, timeout=5
28
+ )
29
+ if os.path.exists(tmp):
30
+ size = os.path.getsize(tmp)
31
+ os.unlink(tmp)
32
+ if size > 100:
33
+ return True
34
+ # Check stderr for permission errors
35
+ if "permission" in proc.stderr.lower() or "not authorized" in proc.stderr.lower():
36
+ return False
37
+ return proc.returncode == 0
38
+ except:
39
+ return False
40
+
41
+
42
+ def setup_voice():
43
+ """Interactive setup for voice input."""
44
+ console.print(f"\n [bold magenta]Voice Input Setup[/]\n")
45
+
46
+ # Step 1: Check sox/rec
47
+ if not os.path.exists(SOX_REC):
48
+ console.print(f" [yellow]Installing sox (audio recorder)...[/]")
49
+ r = subprocess.run(["brew", "install", "sox"], timeout=120)
50
+ if r.returncode != 0:
51
+ console.print(f" [red]Failed to install sox. Run: brew install sox[/]")
52
+ return False
53
+ console.print(f" [green]✓ sox installed[/]")
54
+ else:
55
+ console.print(f" [green]✓ sox already installed[/]")
56
+
57
+ # Step 2: Check whisper-cli
58
+ whisper_bin = shutil.which("whisper-cli")
59
+ if not whisper_bin:
60
+ console.print(f" [yellow]Installing whisper-cpp...[/]")
61
+ r = subprocess.run(["brew", "install", "whisper-cpp"], timeout=300)
62
+ if r.returncode != 0:
63
+ console.print(f" [red]Failed to install whisper-cpp. Run: brew install whisper-cpp[/]")
64
+ return False
65
+ console.print(f" [green]✓ whisper-cpp installed[/]")
66
+ else:
67
+ console.print(f" [green]✓ whisper-cpp already installed[/]")
68
+
69
+ # Step 3: Download whisper model
70
+ if not WHISPER_MODEL.exists():
71
+ console.print(f" [yellow]Downloading whisper base model (148MB)...[/]")
72
+ console.print(f" [dim]Supports: English, French, Arabic + 95 more languages[/]")
73
+ WHISPER_MODEL_DIR.mkdir(parents=True, exist_ok=True)
74
+ r = subprocess.run([
75
+ "curl", "-L", "--progress-bar",
76
+ "-o", str(WHISPER_MODEL),
77
+ "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-base.bin"
78
+ ], timeout=300)
79
+ if r.returncode != 0 or not WHISPER_MODEL.exists():
80
+ console.print(f" [red]Failed to download whisper model[/]")
81
+ return False
82
+ console.print(f" [green]✓ Whisper base model downloaded ({WHISPER_MODEL.stat().st_size // 1024 // 1024}MB)[/]")
83
+ else:
84
+ console.print(f" [green]✓ Whisper model ready ({WHISPER_MODEL.stat().st_size // 1024 // 1024}MB)[/]")
85
+
86
+ # Step 4: Check microphone permission
87
+ console.print(f"\n [dim]Testing microphone access...[/]")
88
+ if check_mic_permission():
89
+ console.print(f" [green]✓ Microphone access granted[/]")
90
+ else:
91
+ console.print(f" [yellow]⚠ Microphone access needed[/]")
92
+ console.print(f" [dim]macOS will prompt for permission on first recording.[/]")
93
+ console.print(f" [dim]If denied, go to: System Settings → Privacy & Security → Microphone[/]")
94
+ console.print(f" [dim]and enable access for your terminal app (iTerm2 / Terminal).[/]")
95
+
96
+ console.print(f"\n [green]✓ Voice input ready![/]")
97
+ console.print(f" [dim]Hold Space while typing prompt to record, release to transcribe.[/]")
98
+ return True
99
+
100
+
101
+ def record_audio(max_seconds=30):
102
+ """Record from mic until stopped. Returns path to WAV file."""
103
+ wav_path = tempfile.mktemp(suffix=".wav")
104
+ proc = subprocess.Popen(
105
+ [SOX_REC, "-q", "-r", "16000", "-c", "1", "-b", "16", wav_path,
106
+ "trim", "0", str(max_seconds)],
107
+ stdin=subprocess.PIPE,
108
+ stdout=subprocess.DEVNULL,
109
+ stderr=subprocess.DEVNULL,
110
+ )
111
+ return proc, wav_path
112
+
113
+
114
+ def stop_recording(proc):
115
+ """Stop the recording process."""
116
+ try:
117
+ proc.send_signal(signal.SIGINT)
118
+ proc.wait(timeout=3)
119
+ except:
120
+ proc.kill()
121
+
122
+
123
+ def transcribe(wav_path, language="auto"):
124
+ """Transcribe audio using whisper-cli (CPU-only, no GPU conflict)."""
125
+ whisper = shutil.which("whisper-cli") or WHISPER_BIN
126
+ if not os.path.exists(whisper):
127
+ return None, "whisper-cli not found"
128
+
129
+ model = str(WHISPER_MODEL)
130
+ if not os.path.exists(model):
131
+ return None, "whisper model not found"
132
+
133
+ try:
134
+ # Use Metal GPU if llama-server has headroom (base model ~200MB),
135
+ # otherwise fall back to CPU-only
136
+ gpu_flags = []
137
+ try:
138
+ # Check free GPU headroom
139
+ out = subprocess.run(["pgrep", "-f", "llama-server"], capture_output=True, text=True)
140
+ if out.stdout.strip():
141
+ # llama-server running — whisper base needs ~200MB, check if safe
142
+ # With 2.7GB headroom on 24GB Mac, Metal whisper is fine
143
+ gpu_flags = [] # let whisper use Metal (default)
144
+ else:
145
+ gpu_flags = [] # no conflict, use Metal
146
+ except:
147
+ gpu_flags = ["--no-gpu"] # safe fallback
148
+
149
+ result = subprocess.run(
150
+ [whisper,
151
+ "--model", model,
152
+ "--language", language,
153
+ "--no-timestamps",
154
+ "--threads", "8",
155
+ "--file", wav_path] + gpu_flags,
156
+ capture_output=True, text=True, timeout=30
157
+ )
158
+
159
+ # Parse output — whisper-cli outputs text lines (with --no-timestamps, just raw text)
160
+ lines = []
161
+ detected_lang = None
162
+ for line in result.stderr.split("\n"):
163
+ if "auto-detected language:" in line:
164
+ detected_lang = line.split("auto-detected language:")[-1].strip().split()[0]
165
+
166
+ for line in result.stdout.split("\n"):
167
+ line = line.strip()
168
+ # Skip empty lines and metadata
169
+ if line and not line.startswith("[") and not line.startswith("whisper_"):
170
+ lines.append(line)
171
+
172
+ text = " ".join(lines).strip()
173
+ # Clean up common whisper artifacts
174
+ text = text.replace("(silence)", "").replace("[BLANK_AUDIO]", "").strip()
175
+
176
+ return text, detected_lang
177
+
178
+ except subprocess.TimeoutExpired:
179
+ return None, "transcription timed out"
180
+ except Exception as e:
181
+ return None, str(e)
182
+ finally:
183
+ if os.path.exists(wav_path):
184
+ try:
185
+ os.unlink(wav_path)
186
+ except:
187
+ pass
@@ -0,0 +1,187 @@
1
+ Metadata-Version: 2.4
2
+ Name: localcoder
3
+ Version: 0.1.0
4
+ Summary: Local AI coding agent — auto-installs, auto-serves, zero config. Works with Gemma 4, Qwen 3.5, and any model via llama.cpp or Ollama.
5
+ Project-URL: Homepage, https://github.com/AnassKartit/localcoder
6
+ Project-URL: Repository, https://github.com/AnassKartit/localcoder
7
+ Author: Anass Kartit
8
+ License-Expression: Apache-2.0
9
+ License-File: LICENSE
10
+ Keywords: agent,ai,coding,gemma4,llama.cpp,local,localcoder,ollama,qwen
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: Apache Software License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: Software Development :: Code Generators
17
+ Requires-Python: >=3.10
18
+ Requires-Dist: huggingface-hub>=0.20
19
+ Requires-Dist: prompt-toolkit>=3.0
20
+ Requires-Dist: rich>=13.0
21
+ Description-Content-Type: text/markdown
22
+
23
+ # localcoder
24
+
25
+ **The local coding CLI that does the obvious things nobody else does.**
26
+
27
+ ```bash
28
+ pipx install localcoder
29
+ ```
30
+
31
+ I wanted to paste a screenshot into my coding assistant and see it inline. No tool did that locally. So I built one.
32
+
33
+ ## Cost: $1.30/month vs $110/month
34
+
35
+ Running local saves 85-141x compared to cloud APIs:
36
+
37
+ | Usage | Claude Sonnet | Claude Opus | Local (US) | Local (India) |
38
+ |-------|--------------|-------------|------------|---------------|
39
+ | 4h/day | $55/mo | $91/mo | **$0.65/mo** | $0.29/mo |
40
+ | 8h/day | $110/mo | $183/mo | **$1.30/mo** | $0.58/mo |
41
+ | 10h/day | $137/mo | $228/mo | **$1.62/mo** | $0.72/mo |
42
+
43
+ *Based on: Gemma 4 26B at 47 tok/s, 30% active generation, M4 Pro 30W. Electricity: [worldpopulationreview.com](https://worldpopulationreview.com/country-rankings/cost-of-electricity-by-country). API: [anthropic.com](https://www.anthropic.com/pricing).*
44
+
45
+ **Annual savings: ~$1,300-$2,700** depending on usage and API choice.
46
+
47
+ ## What's Actually Different
48
+
49
+ | Feature | localcoder | aider | OpenCode | Claude Code |
50
+ |---------|-----------|-------|----------|-------------|
51
+ | Paste image, see it inline | **Ctrl+V → shows in terminal** | no | no | cloud only |
52
+ | Voice input (local) | **Ctrl+R → Whisper, no cloud** | no | no | no |
53
+ | See GPU memory while coding | **/gpu → live stats** | no | no | no |
54
+ | Computer use (screenshot + click) | **built-in** | no | no | cloud only |
55
+ | Free GPU when it's slow | **/clean → before/after** | no | no | n/a |
56
+ | Browse HuggingFace models | **built-in model browser** | no | no | n/a |
57
+ | Works offline | **100%** | partial | partial | no |
58
+ | Cost | **$0.00** | API costs | API costs | $20/mo+ |
59
+
60
+ ## Demo
61
+
62
+ ```
63
+ ❯ localcoder
64
+
65
+ localcoder · local AI coding agent · $0.00 forever
66
+
67
+ ┌──────────────────────────────────────────────────┐
68
+ │ LOCAL CODER │
69
+ └──────────────────────────────────────────────────┘
70
+
71
+ ● Gemma 4 26B Q3_K_XL · llama.cpp · 128K · ● GPU · 47 tok/s
72
+ ✓ offline · no API keys · no data sent
73
+
74
+ ctrl+r voice ctrl+v image /gpu stats /clean free /models switch
75
+
76
+ ❯ /gpu
77
+ GPU ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 12/16GB 3GB free
78
+ Swap 3GB Pressure normal
79
+ Model Gemma 4 26B Q3_K_XL GPU ctx 128K footprint 2311MB
80
+ ```
81
+
82
+ ## Benchmark — M4 Pro 24GB
83
+
84
+ Real tests, real hardware, no synthetic benchmarks:
85
+
86
+ | Model | Size | tok/s | Notes |
87
+ |-------|------|-------|-------|
88
+ | **Gemma 4 26B** Q3_K_XL | 12.0GB | 47 | Best overall — vision + tool calling |
89
+ | **Qwen3.5-35B** MoE Q2_K_XL | 11.3GB | 46 | Best coding quality |
90
+ | **Qwen3.5-4B** Q4_K_XL | 2.7GB | 46 | Quick tasks |
91
+ | Gemma 4 E4B Q4_K_M | 5.0GB | 56 | Fastest — good for 16GB Macs |
92
+ | ~~Qwen3.5-27B Dense~~ | ~~13.4GB~~ | ~~7~~ | ~~Swap thrashing — don't use on 24GB~~ |
93
+
94
+ ## Install
95
+
96
+ ```bash
97
+ # macOS (Apple Silicon)
98
+ pipx install localcoder
99
+
100
+ # First run — auto-detects hardware, shows what fits, starts model
101
+ localcoder
102
+ ```
103
+
104
+ Needs [llama.cpp](https://github.com/ggml-org/llama.cpp) or [Ollama](https://ollama.com). First run wizard handles this.
105
+
106
+ ## Commands
107
+
108
+ ```bash
109
+ localcoder # interactive coding
110
+ localcoder -p "build a react app" # one-shot
111
+ localcoder --yolo # auto-approve tools
112
+ ```
113
+
114
+ ### While Coding
115
+
116
+ | Command | What |
117
+ |---------|------|
118
+ | `Ctrl+V` | Paste + display image from clipboard |
119
+ | `Ctrl+R` | Toggle voice input (local Whisper) |
120
+ | `/gpu` | GPU memory, swap, model status |
121
+ | `/clean` | Free GPU memory with before/after |
122
+ | `/models` | Switch model (includes HuggingFace trending) |
123
+ | `/clear` | Clear conversation |
124
+
125
+ ### Also works with Claude Code
126
+
127
+ Don't want localcoder's agent? Use Claude Code with your local model instead:
128
+
129
+ ```bash
130
+ pip install localfit
131
+ localfit --launch claude --model gemma4-26b
132
+ ```
133
+
134
+ One command: starts model → configures Claude Code → launches with `--bare` flag.
135
+ See [localfit](https://github.com/AnassKartit/localfit) for details.
136
+
137
+ ### GPU Toolkit (localfit inside)
138
+
139
+ ```bash
140
+ localcoder --simulate # will this model fit my GPU?
141
+ localcoder --fetch unsloth/... # check all quants from HuggingFace
142
+ localcoder --bench # benchmark models on YOUR hardware
143
+ localcoder --health # GPU health dashboard
144
+ localcoder --config opencode # auto-configure OpenCode for local models
145
+ localcoder --config aider # auto-configure aider
146
+ ```
147
+
148
+ Also available standalone: `pipx install localfit`
149
+
150
+ ## Hardware
151
+
152
+ | Mac | RAM | Best Model | Speed |
153
+ |-----|-----|-----------|-------|
154
+ | Air M2 | 8 GB | Qwen 3.5 4B | 50 tok/s |
155
+ | Air M3 | 16 GB | Gemma 4 E4B | 57 tok/s |
156
+ | **Pro M4** | **24 GB** | **Gemma 4 26B Q3_K_XL** | **47 tok/s** |
157
+
158
+ ## License
159
+
160
+ Apache-2.0
161
+
162
+ ## Security
163
+
164
+ Sandbox mode is **ON by default**. Protects against destructive model outputs:
165
+
166
+ | Blocked | Examples |
167
+ |---------|----------|
168
+ | Destructive commands | `rm -rf`, `sudo`, `kill`, `mkfs` |
169
+ | Pipe to shell | `curl ... \| bash`, `wget ... \| sh` |
170
+ | Protected paths | `~/.ssh`, `~/.aws`, `~/.env`, `/etc/` |
171
+ | Path traversal | `../../etc/passwd` |
172
+ | Computer use | Disabled in sandbox |
173
+
174
+ ```bash
175
+ localcoder # sandboxed (default)
176
+ localcoder --yolo # auto-approve but sandbox ON
177
+ localcoder --unrestricted # sandbox OFF (shows warning)
178
+ ```
179
+
180
+ Approved tools are remembered across sessions (`~/.localcoder/approved_tools.json`).
181
+
182
+ ## Tests
183
+
184
+ ```bash
185
+ pip install pytest
186
+ pytest tests/ -v # 19 tests
187
+ ```
@@ -0,0 +1,15 @@
1
+ localcoder/__init__.py,sha256=eD-uC7S1gz0HbdaaORS2soj6zh5BB_tytoArAKfoioI,111
2
+ localcoder/__main__.py,sha256=vFZX_R8P6RBS2CiBSvkFy_IG9b7wOi8d9h5H-y9Eskw,39
3
+ localcoder/agent.py,sha256=tLaWfkgAmq-dQ9PqO59RziPS7deijg3HcJ1jmAyqWnM,1044
4
+ localcoder/backends.py,sha256=JeGS-mzIWPElWmrZnswotInC225YhcFkRNztNI-3mhE,96500
5
+ localcoder/bench.py,sha256=xVDRENdCrjy_Mz2Q_PeEax8ws_-6uTKcKYXmBm1eqr4,12620
6
+ localcoder/cli.py,sha256=Yj6s02gQi37ctWydNt8VJHG0Zl1YdcZ4tM828oXWEeM,38917
7
+ localcoder/gemma4coder_display.py,sha256=gGnVqmSpurOoBw_cup-ABWC_SzetWv1rk1KBz23fPck,17693
8
+ localcoder/setup.py,sha256=3ACjQSOSmgPM9qhd5eaPAQ5xmGdYSB5C1y3EavRiw8M,11464
9
+ localcoder/tui.py,sha256=wwOV8IrZumgg4QF1MCafHPtoCc5Av6sBoQoAqYnhoYU,9021
10
+ localcoder/voice.py,sha256=ZEBFWzSB0tFoN8OQ68sV_jPPT_uMXcn95HoCS28ZmWw,7246
11
+ localcoder-0.1.0.dist-info/METADATA,sha256=FOdoZ3sHADRJShBvD6-TaQO1hsQM5u-j3K-25_Rb9P0,6684
12
+ localcoder-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
13
+ localcoder-0.1.0.dist-info/entry_points.txt,sha256=svs7yRCAJO8U8945pL-hic2eQnMfqRQx8v_m9U5J5Xo,51
14
+ localcoder-0.1.0.dist-info/licenses/LICENSE,sha256=7KCxSOg3vOVwKw_kVsnzWCY7y0FNv4X_a0r__G0Q6hY,95
15
+ localcoder-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ localcoder = localcoder.cli:main
@@ -0,0 +1,4 @@
1
+ Apache License 2.0
2
+ Copyright 2026 Anass Kartit
3
+
4
+ Licensed under the Apache License, Version 2.0