aider-ce 0.88.20__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.
- aider/__init__.py +20 -0
- aider/__main__.py +4 -0
- aider/_version.py +34 -0
- aider/analytics.py +258 -0
- aider/args.py +1056 -0
- aider/args_formatter.py +228 -0
- aider/change_tracker.py +133 -0
- aider/coders/__init__.py +36 -0
- aider/coders/agent_coder.py +2166 -0
- aider/coders/agent_prompts.py +104 -0
- aider/coders/architect_coder.py +48 -0
- aider/coders/architect_prompts.py +40 -0
- aider/coders/ask_coder.py +9 -0
- aider/coders/ask_prompts.py +35 -0
- aider/coders/base_coder.py +3613 -0
- aider/coders/base_prompts.py +87 -0
- aider/coders/chat_chunks.py +64 -0
- aider/coders/context_coder.py +53 -0
- aider/coders/context_prompts.py +75 -0
- aider/coders/editblock_coder.py +657 -0
- aider/coders/editblock_fenced_coder.py +10 -0
- aider/coders/editblock_fenced_prompts.py +143 -0
- aider/coders/editblock_func_coder.py +141 -0
- aider/coders/editblock_func_prompts.py +27 -0
- aider/coders/editblock_prompts.py +175 -0
- aider/coders/editor_diff_fenced_coder.py +9 -0
- aider/coders/editor_diff_fenced_prompts.py +11 -0
- aider/coders/editor_editblock_coder.py +9 -0
- aider/coders/editor_editblock_prompts.py +21 -0
- aider/coders/editor_whole_coder.py +9 -0
- aider/coders/editor_whole_prompts.py +12 -0
- aider/coders/help_coder.py +16 -0
- aider/coders/help_prompts.py +46 -0
- aider/coders/patch_coder.py +706 -0
- aider/coders/patch_prompts.py +159 -0
- aider/coders/search_replace.py +757 -0
- aider/coders/shell.py +37 -0
- aider/coders/single_wholefile_func_coder.py +102 -0
- aider/coders/single_wholefile_func_prompts.py +27 -0
- aider/coders/udiff_coder.py +429 -0
- aider/coders/udiff_prompts.py +115 -0
- aider/coders/udiff_simple.py +14 -0
- aider/coders/udiff_simple_prompts.py +25 -0
- aider/coders/wholefile_coder.py +144 -0
- aider/coders/wholefile_func_coder.py +134 -0
- aider/coders/wholefile_func_prompts.py +27 -0
- aider/coders/wholefile_prompts.py +65 -0
- aider/commands.py +2173 -0
- aider/copypaste.py +72 -0
- aider/deprecated.py +126 -0
- aider/diffs.py +128 -0
- aider/dump.py +29 -0
- aider/editor.py +147 -0
- aider/exceptions.py +115 -0
- aider/format_settings.py +26 -0
- aider/gui.py +545 -0
- aider/help.py +163 -0
- aider/help_pats.py +19 -0
- aider/helpers/__init__.py +9 -0
- aider/helpers/similarity.py +98 -0
- aider/history.py +180 -0
- aider/io.py +1608 -0
- aider/linter.py +304 -0
- aider/llm.py +55 -0
- aider/main.py +1415 -0
- aider/mcp/__init__.py +174 -0
- aider/mcp/server.py +149 -0
- aider/mdstream.py +243 -0
- aider/models.py +1313 -0
- aider/onboarding.py +429 -0
- aider/openrouter.py +129 -0
- aider/prompts.py +56 -0
- aider/queries/tree-sitter-language-pack/README.md +7 -0
- aider/queries/tree-sitter-language-pack/arduino-tags.scm +5 -0
- aider/queries/tree-sitter-language-pack/c-tags.scm +9 -0
- aider/queries/tree-sitter-language-pack/chatito-tags.scm +16 -0
- aider/queries/tree-sitter-language-pack/clojure-tags.scm +7 -0
- aider/queries/tree-sitter-language-pack/commonlisp-tags.scm +122 -0
- aider/queries/tree-sitter-language-pack/cpp-tags.scm +15 -0
- aider/queries/tree-sitter-language-pack/csharp-tags.scm +26 -0
- aider/queries/tree-sitter-language-pack/d-tags.scm +26 -0
- aider/queries/tree-sitter-language-pack/dart-tags.scm +92 -0
- aider/queries/tree-sitter-language-pack/elisp-tags.scm +5 -0
- aider/queries/tree-sitter-language-pack/elixir-tags.scm +54 -0
- aider/queries/tree-sitter-language-pack/elm-tags.scm +19 -0
- aider/queries/tree-sitter-language-pack/gleam-tags.scm +41 -0
- aider/queries/tree-sitter-language-pack/go-tags.scm +42 -0
- aider/queries/tree-sitter-language-pack/java-tags.scm +20 -0
- aider/queries/tree-sitter-language-pack/javascript-tags.scm +88 -0
- aider/queries/tree-sitter-language-pack/lua-tags.scm +34 -0
- aider/queries/tree-sitter-language-pack/matlab-tags.scm +10 -0
- aider/queries/tree-sitter-language-pack/ocaml-tags.scm +115 -0
- aider/queries/tree-sitter-language-pack/ocaml_interface-tags.scm +98 -0
- aider/queries/tree-sitter-language-pack/pony-tags.scm +39 -0
- aider/queries/tree-sitter-language-pack/properties-tags.scm +5 -0
- aider/queries/tree-sitter-language-pack/python-tags.scm +14 -0
- aider/queries/tree-sitter-language-pack/r-tags.scm +21 -0
- aider/queries/tree-sitter-language-pack/racket-tags.scm +12 -0
- aider/queries/tree-sitter-language-pack/ruby-tags.scm +64 -0
- aider/queries/tree-sitter-language-pack/rust-tags.scm +60 -0
- aider/queries/tree-sitter-language-pack/solidity-tags.scm +43 -0
- aider/queries/tree-sitter-language-pack/swift-tags.scm +51 -0
- aider/queries/tree-sitter-language-pack/udev-tags.scm +20 -0
- aider/queries/tree-sitter-languages/README.md +24 -0
- aider/queries/tree-sitter-languages/c-tags.scm +9 -0
- aider/queries/tree-sitter-languages/c_sharp-tags.scm +46 -0
- aider/queries/tree-sitter-languages/cpp-tags.scm +15 -0
- aider/queries/tree-sitter-languages/dart-tags.scm +91 -0
- aider/queries/tree-sitter-languages/elisp-tags.scm +8 -0
- aider/queries/tree-sitter-languages/elixir-tags.scm +54 -0
- aider/queries/tree-sitter-languages/elm-tags.scm +19 -0
- aider/queries/tree-sitter-languages/fortran-tags.scm +15 -0
- aider/queries/tree-sitter-languages/go-tags.scm +30 -0
- aider/queries/tree-sitter-languages/haskell-tags.scm +3 -0
- aider/queries/tree-sitter-languages/hcl-tags.scm +77 -0
- aider/queries/tree-sitter-languages/java-tags.scm +20 -0
- aider/queries/tree-sitter-languages/javascript-tags.scm +88 -0
- aider/queries/tree-sitter-languages/julia-tags.scm +60 -0
- aider/queries/tree-sitter-languages/kotlin-tags.scm +27 -0
- aider/queries/tree-sitter-languages/matlab-tags.scm +10 -0
- aider/queries/tree-sitter-languages/ocaml-tags.scm +115 -0
- aider/queries/tree-sitter-languages/ocaml_interface-tags.scm +98 -0
- aider/queries/tree-sitter-languages/php-tags.scm +26 -0
- aider/queries/tree-sitter-languages/python-tags.scm +12 -0
- aider/queries/tree-sitter-languages/ql-tags.scm +26 -0
- aider/queries/tree-sitter-languages/ruby-tags.scm +64 -0
- aider/queries/tree-sitter-languages/rust-tags.scm +60 -0
- aider/queries/tree-sitter-languages/scala-tags.scm +65 -0
- aider/queries/tree-sitter-languages/typescript-tags.scm +41 -0
- aider/queries/tree-sitter-languages/zig-tags.scm +3 -0
- aider/reasoning_tags.py +82 -0
- aider/repo.py +621 -0
- aider/repomap.py +1174 -0
- aider/report.py +260 -0
- aider/resources/__init__.py +3 -0
- aider/resources/model-metadata.json +776 -0
- aider/resources/model-settings.yml +2068 -0
- aider/run_cmd.py +133 -0
- aider/scrape.py +293 -0
- aider/sendchat.py +242 -0
- aider/sessions.py +256 -0
- aider/special.py +203 -0
- aider/tools/__init__.py +72 -0
- aider/tools/command.py +105 -0
- aider/tools/command_interactive.py +122 -0
- aider/tools/delete_block.py +182 -0
- aider/tools/delete_line.py +155 -0
- aider/tools/delete_lines.py +184 -0
- aider/tools/extract_lines.py +341 -0
- aider/tools/finished.py +48 -0
- aider/tools/git_branch.py +129 -0
- aider/tools/git_diff.py +60 -0
- aider/tools/git_log.py +57 -0
- aider/tools/git_remote.py +53 -0
- aider/tools/git_show.py +51 -0
- aider/tools/git_status.py +46 -0
- aider/tools/grep.py +256 -0
- aider/tools/indent_lines.py +221 -0
- aider/tools/insert_block.py +288 -0
- aider/tools/list_changes.py +86 -0
- aider/tools/ls.py +93 -0
- aider/tools/make_editable.py +85 -0
- aider/tools/make_readonly.py +69 -0
- aider/tools/remove.py +91 -0
- aider/tools/replace_all.py +126 -0
- aider/tools/replace_line.py +173 -0
- aider/tools/replace_lines.py +217 -0
- aider/tools/replace_text.py +187 -0
- aider/tools/show_numbered_context.py +147 -0
- aider/tools/tool_utils.py +313 -0
- aider/tools/undo_change.py +95 -0
- aider/tools/update_todo_list.py +156 -0
- aider/tools/view.py +57 -0
- aider/tools/view_files_matching.py +141 -0
- aider/tools/view_files_with_symbol.py +129 -0
- aider/urls.py +17 -0
- aider/utils.py +456 -0
- aider/versioncheck.py +113 -0
- aider/voice.py +205 -0
- aider/waiting.py +38 -0
- aider/watch.py +318 -0
- aider/watch_prompts.py +12 -0
- aider/website/Gemfile +8 -0
- aider/website/_includes/blame.md +162 -0
- aider/website/_includes/get-started.md +22 -0
- aider/website/_includes/help-tip.md +5 -0
- aider/website/_includes/help.md +24 -0
- aider/website/_includes/install.md +5 -0
- aider/website/_includes/keys.md +4 -0
- aider/website/_includes/model-warnings.md +67 -0
- aider/website/_includes/multi-line.md +22 -0
- aider/website/_includes/python-m-aider.md +5 -0
- aider/website/_includes/recording.css +228 -0
- aider/website/_includes/recording.md +34 -0
- aider/website/_includes/replit-pipx.md +9 -0
- aider/website/_includes/works-best.md +1 -0
- aider/website/_sass/custom/custom.scss +103 -0
- aider/website/docs/config/adv-model-settings.md +2261 -0
- aider/website/docs/config/agent-mode.md +194 -0
- aider/website/docs/config/aider_conf.md +548 -0
- aider/website/docs/config/api-keys.md +90 -0
- aider/website/docs/config/dotenv.md +493 -0
- aider/website/docs/config/editor.md +127 -0
- aider/website/docs/config/mcp.md +95 -0
- aider/website/docs/config/model-aliases.md +104 -0
- aider/website/docs/config/options.md +890 -0
- aider/website/docs/config/reasoning.md +210 -0
- aider/website/docs/config.md +44 -0
- aider/website/docs/faq.md +384 -0
- aider/website/docs/git.md +76 -0
- aider/website/docs/index.md +47 -0
- aider/website/docs/install/codespaces.md +39 -0
- aider/website/docs/install/docker.md +57 -0
- aider/website/docs/install/optional.md +100 -0
- aider/website/docs/install/replit.md +8 -0
- aider/website/docs/install.md +115 -0
- aider/website/docs/languages.md +264 -0
- aider/website/docs/legal/contributor-agreement.md +111 -0
- aider/website/docs/legal/privacy.md +104 -0
- aider/website/docs/llms/anthropic.md +77 -0
- aider/website/docs/llms/azure.md +48 -0
- aider/website/docs/llms/bedrock.md +132 -0
- aider/website/docs/llms/cohere.md +34 -0
- aider/website/docs/llms/deepseek.md +32 -0
- aider/website/docs/llms/gemini.md +49 -0
- aider/website/docs/llms/github.md +111 -0
- aider/website/docs/llms/groq.md +36 -0
- aider/website/docs/llms/lm-studio.md +39 -0
- aider/website/docs/llms/ollama.md +75 -0
- aider/website/docs/llms/openai-compat.md +39 -0
- aider/website/docs/llms/openai.md +58 -0
- aider/website/docs/llms/openrouter.md +78 -0
- aider/website/docs/llms/other.md +117 -0
- aider/website/docs/llms/vertex.md +50 -0
- aider/website/docs/llms/warnings.md +10 -0
- aider/website/docs/llms/xai.md +53 -0
- aider/website/docs/llms.md +54 -0
- aider/website/docs/more/analytics.md +127 -0
- aider/website/docs/more/edit-formats.md +116 -0
- aider/website/docs/more/infinite-output.md +165 -0
- aider/website/docs/more-info.md +8 -0
- aider/website/docs/recordings/auto-accept-architect.md +31 -0
- aider/website/docs/recordings/dont-drop-original-read-files.md +35 -0
- aider/website/docs/recordings/index.md +21 -0
- aider/website/docs/recordings/model-accepts-settings.md +69 -0
- aider/website/docs/recordings/tree-sitter-language-pack.md +80 -0
- aider/website/docs/repomap.md +112 -0
- aider/website/docs/scripting.md +100 -0
- aider/website/docs/sessions.md +203 -0
- aider/website/docs/troubleshooting/aider-not-found.md +24 -0
- aider/website/docs/troubleshooting/edit-errors.md +76 -0
- aider/website/docs/troubleshooting/imports.md +62 -0
- aider/website/docs/troubleshooting/models-and-keys.md +54 -0
- aider/website/docs/troubleshooting/support.md +79 -0
- aider/website/docs/troubleshooting/token-limits.md +96 -0
- aider/website/docs/troubleshooting/warnings.md +12 -0
- aider/website/docs/troubleshooting.md +11 -0
- aider/website/docs/usage/browser.md +57 -0
- aider/website/docs/usage/caching.md +49 -0
- aider/website/docs/usage/commands.md +133 -0
- aider/website/docs/usage/conventions.md +119 -0
- aider/website/docs/usage/copypaste.md +121 -0
- aider/website/docs/usage/images-urls.md +48 -0
- aider/website/docs/usage/lint-test.md +118 -0
- aider/website/docs/usage/modes.md +211 -0
- aider/website/docs/usage/not-code.md +179 -0
- aider/website/docs/usage/notifications.md +87 -0
- aider/website/docs/usage/tips.md +79 -0
- aider/website/docs/usage/tutorials.md +30 -0
- aider/website/docs/usage/voice.md +121 -0
- aider/website/docs/usage/watch.md +294 -0
- aider/website/docs/usage.md +102 -0
- aider/website/share/index.md +101 -0
- aider_ce-0.88.20.dist-info/METADATA +187 -0
- aider_ce-0.88.20.dist-info/RECORD +279 -0
- aider_ce-0.88.20.dist-info/WHEEL +5 -0
- aider_ce-0.88.20.dist-info/entry_points.txt +2 -0
- aider_ce-0.88.20.dist-info/licenses/LICENSE.txt +202 -0
- aider_ce-0.88.20.dist-info/top_level.txt +1 -0
aider/voice.py
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import math
|
|
2
|
+
import os
|
|
3
|
+
import queue
|
|
4
|
+
import tempfile
|
|
5
|
+
import time
|
|
6
|
+
import warnings
|
|
7
|
+
|
|
8
|
+
from prompt_toolkit.shortcuts import prompt
|
|
9
|
+
|
|
10
|
+
from aider.llm import litellm
|
|
11
|
+
|
|
12
|
+
from .dump import dump # noqa: F401
|
|
13
|
+
|
|
14
|
+
warnings.filterwarnings(
|
|
15
|
+
"ignore", message="Couldn't find ffmpeg or avconv - defaulting to ffmpeg, but may not work"
|
|
16
|
+
)
|
|
17
|
+
warnings.filterwarnings("ignore", category=SyntaxWarning)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
from pydub import AudioSegment # noqa
|
|
22
|
+
from pydub.exceptions import CouldntDecodeError, CouldntEncodeError # noqa
|
|
23
|
+
|
|
24
|
+
PYDUB_AVAILABLE = True
|
|
25
|
+
except (ModuleNotFoundError, ImportError) as e:
|
|
26
|
+
if "audioop" in str(e) or "pyaudioop" in str(e):
|
|
27
|
+
# Handle missing audioop/pyaudioop dependency gracefully
|
|
28
|
+
PYDUB_AVAILABLE = False
|
|
29
|
+
AudioSegment = None
|
|
30
|
+
CouldntDecodeError = Exception
|
|
31
|
+
CouldntEncodeError = Exception
|
|
32
|
+
else:
|
|
33
|
+
raise
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
import soundfile as sf
|
|
37
|
+
except (OSError, ModuleNotFoundError):
|
|
38
|
+
sf = None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class SoundDeviceError(Exception):
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class Voice:
|
|
46
|
+
max_rms = 0
|
|
47
|
+
min_rms = 1e5
|
|
48
|
+
pct = 0
|
|
49
|
+
|
|
50
|
+
threshold = 0.15
|
|
51
|
+
|
|
52
|
+
def __init__(self, audio_format="wav", device_name=None):
|
|
53
|
+
if sf is None:
|
|
54
|
+
raise SoundDeviceError
|
|
55
|
+
try:
|
|
56
|
+
print("Initializing sound device...")
|
|
57
|
+
import sounddevice as sd
|
|
58
|
+
|
|
59
|
+
self.sd = sd
|
|
60
|
+
|
|
61
|
+
devices = sd.query_devices()
|
|
62
|
+
|
|
63
|
+
if device_name:
|
|
64
|
+
# Find the device with matching name
|
|
65
|
+
device_id = None
|
|
66
|
+
for i, device in enumerate(devices):
|
|
67
|
+
if device_name in device["name"]:
|
|
68
|
+
device_id = i
|
|
69
|
+
break
|
|
70
|
+
if device_id is None:
|
|
71
|
+
available_inputs = [d["name"] for d in devices if d["max_input_channels"] > 0]
|
|
72
|
+
raise ValueError(
|
|
73
|
+
f"Device '{device_name}' not found. Available input devices:"
|
|
74
|
+
f" {available_inputs}"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
print(f"Using input device: {device_name} (ID: {device_id})")
|
|
78
|
+
|
|
79
|
+
self.device_id = device_id
|
|
80
|
+
else:
|
|
81
|
+
self.device_id = None
|
|
82
|
+
|
|
83
|
+
except (OSError, ModuleNotFoundError):
|
|
84
|
+
raise SoundDeviceError
|
|
85
|
+
if audio_format not in ["wav", "mp3", "webm"]:
|
|
86
|
+
raise ValueError(f"Unsupported audio format: {audio_format}")
|
|
87
|
+
self.audio_format = audio_format
|
|
88
|
+
|
|
89
|
+
def callback(self, indata, frames, time, status):
|
|
90
|
+
"""This is called (from a separate thread) for each audio block."""
|
|
91
|
+
import numpy as np
|
|
92
|
+
|
|
93
|
+
rms = np.sqrt(np.mean(indata**2))
|
|
94
|
+
self.max_rms = max(self.max_rms, rms)
|
|
95
|
+
self.min_rms = min(self.min_rms, rms)
|
|
96
|
+
|
|
97
|
+
rng = self.max_rms - self.min_rms
|
|
98
|
+
if rng > 0.001:
|
|
99
|
+
self.pct = (rms - self.min_rms) / rng
|
|
100
|
+
else:
|
|
101
|
+
self.pct = 0.5
|
|
102
|
+
|
|
103
|
+
self.q.put(indata.copy())
|
|
104
|
+
|
|
105
|
+
def get_prompt(self):
|
|
106
|
+
num = 10
|
|
107
|
+
if math.isnan(self.pct) or self.pct < self.threshold:
|
|
108
|
+
cnt = 0
|
|
109
|
+
else:
|
|
110
|
+
cnt = int(self.pct * 10)
|
|
111
|
+
|
|
112
|
+
bar = "░" * cnt + "█" * (num - cnt)
|
|
113
|
+
bar = bar[:num]
|
|
114
|
+
|
|
115
|
+
dur = time.time() - self.start_time
|
|
116
|
+
return f"Recording, press ENTER when done... {dur:.1f}sec {bar}"
|
|
117
|
+
|
|
118
|
+
def record_and_transcribe(self, history=None, language=None):
|
|
119
|
+
try:
|
|
120
|
+
return self.raw_record_and_transcribe(history, language)
|
|
121
|
+
except KeyboardInterrupt:
|
|
122
|
+
return
|
|
123
|
+
except SoundDeviceError as e:
|
|
124
|
+
print(f"Error: {e}")
|
|
125
|
+
print("Please ensure you have a working audio input device connected and try again.")
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
def raw_record_and_transcribe(self, history, language):
|
|
129
|
+
self.q = queue.Queue()
|
|
130
|
+
|
|
131
|
+
temp_wav = tempfile.mktemp(suffix=".wav")
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
sample_rate = int(self.sd.query_devices(self.device_id, "input")["default_samplerate"])
|
|
135
|
+
except (TypeError, ValueError):
|
|
136
|
+
sample_rate = 16000 # fallback to 16kHz if unable to query device
|
|
137
|
+
except self.sd.PortAudioError:
|
|
138
|
+
raise SoundDeviceError(
|
|
139
|
+
"No audio input device detected. Please check your audio settings and try again."
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
self.start_time = time.time()
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
with self.sd.InputStream(
|
|
146
|
+
samplerate=sample_rate, channels=1, callback=self.callback, device=self.device_id
|
|
147
|
+
):
|
|
148
|
+
prompt(self.get_prompt, refresh_interval=0.1)
|
|
149
|
+
except self.sd.PortAudioError as err:
|
|
150
|
+
raise SoundDeviceError(f"Error accessing audio input device: {err}")
|
|
151
|
+
|
|
152
|
+
with sf.SoundFile(temp_wav, mode="x", samplerate=sample_rate, channels=1) as file:
|
|
153
|
+
while not self.q.empty():
|
|
154
|
+
file.write(self.q.get())
|
|
155
|
+
|
|
156
|
+
use_audio_format = self.audio_format
|
|
157
|
+
|
|
158
|
+
# Check file size and offer to convert to mp3 if too large
|
|
159
|
+
file_size = os.path.getsize(temp_wav)
|
|
160
|
+
if file_size > 24.9 * 1024 * 1024 and self.audio_format == "wav":
|
|
161
|
+
print("\nWarning: {temp_wav} is too large, switching to mp3 format.")
|
|
162
|
+
use_audio_format = "mp3"
|
|
163
|
+
|
|
164
|
+
filename = temp_wav
|
|
165
|
+
if use_audio_format != "wav":
|
|
166
|
+
try:
|
|
167
|
+
if not PYDUB_AVAILABLE:
|
|
168
|
+
print(
|
|
169
|
+
f"Warning: pydub not available, cannot convert to {use_audio_format}. Using"
|
|
170
|
+
" original WAV file."
|
|
171
|
+
)
|
|
172
|
+
else:
|
|
173
|
+
new_filename = tempfile.mktemp(suffix=f".{use_audio_format}")
|
|
174
|
+
audio = AudioSegment.from_wav(temp_wav)
|
|
175
|
+
audio.export(new_filename, format=use_audio_format)
|
|
176
|
+
os.remove(temp_wav)
|
|
177
|
+
filename = new_filename
|
|
178
|
+
except (CouldntDecodeError, CouldntEncodeError) as e:
|
|
179
|
+
print(f"Error converting audio: {e}")
|
|
180
|
+
except (OSError, FileNotFoundError) as e:
|
|
181
|
+
print(f"File system error during conversion: {e}")
|
|
182
|
+
except Exception as e:
|
|
183
|
+
print(f"Unexpected error during audio conversion: {e}")
|
|
184
|
+
|
|
185
|
+
with open(filename, "rb") as fh:
|
|
186
|
+
try:
|
|
187
|
+
transcript = litellm.transcription(
|
|
188
|
+
model="whisper-1", file=fh, prompt=history, language=language
|
|
189
|
+
)
|
|
190
|
+
except Exception as err:
|
|
191
|
+
print(f"Unable to transcribe {filename}: {err}")
|
|
192
|
+
return
|
|
193
|
+
|
|
194
|
+
if filename != temp_wav:
|
|
195
|
+
os.remove(filename)
|
|
196
|
+
|
|
197
|
+
text = transcript.text
|
|
198
|
+
return text
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
if __name__ == "__main__":
|
|
202
|
+
api_key = os.getenv("OPENAI_API_KEY")
|
|
203
|
+
if not api_key:
|
|
204
|
+
raise ValueError("Please set the OPENAI_API_KEY environment variable.")
|
|
205
|
+
print(Voice().record_and_transcribe())
|
aider/waiting.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
A simple wrapper for rich.status to provide a spinner.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Spinner:
|
|
11
|
+
"""A wrapper around rich.status.Status for displaying a spinner."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, text: str = "Waiting..."):
|
|
14
|
+
self.text = text
|
|
15
|
+
self.console = Console()
|
|
16
|
+
self.status = None
|
|
17
|
+
|
|
18
|
+
def step(self, message=None):
|
|
19
|
+
"""Start the spinner or update its text."""
|
|
20
|
+
if self.status is None:
|
|
21
|
+
self.status = self.console.status(self.text, spinner="dots2")
|
|
22
|
+
self.status.start()
|
|
23
|
+
elif message:
|
|
24
|
+
self.status.update(message)
|
|
25
|
+
|
|
26
|
+
def end(self):
|
|
27
|
+
"""Stop the spinner."""
|
|
28
|
+
if self.status:
|
|
29
|
+
self.status.stop()
|
|
30
|
+
self.status = None
|
|
31
|
+
|
|
32
|
+
# Allow use as a context-manager
|
|
33
|
+
def __enter__(self):
|
|
34
|
+
self.step()
|
|
35
|
+
return self
|
|
36
|
+
|
|
37
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
38
|
+
self.end()
|
aider/watch.py
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import threading
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from grep_ast import TreeContext
|
|
7
|
+
from pathspec import PathSpec
|
|
8
|
+
from pathspec.patterns import GitWildMatchPattern
|
|
9
|
+
from watchfiles import watch
|
|
10
|
+
|
|
11
|
+
from aider.dump import dump # noqa
|
|
12
|
+
from aider.watch_prompts import watch_ask_prompt, watch_code_prompt
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def load_gitignores(gitignore_paths: list[Path]) -> Optional[PathSpec]:
|
|
16
|
+
"""Load and parse multiple .gitignore files into a single PathSpec"""
|
|
17
|
+
if not gitignore_paths:
|
|
18
|
+
return None
|
|
19
|
+
|
|
20
|
+
patterns = [
|
|
21
|
+
".aider*",
|
|
22
|
+
".git",
|
|
23
|
+
# Common editor backup/temp files
|
|
24
|
+
"*~", # Emacs/vim backup
|
|
25
|
+
"*.bak", # Generic backup
|
|
26
|
+
"*.swp", # Vim swap
|
|
27
|
+
"*.swo", # Vim swap
|
|
28
|
+
"\\#*\\#", # Emacs auto-save
|
|
29
|
+
".#*", # Emacs lock files
|
|
30
|
+
"*.tmp", # Generic temp files
|
|
31
|
+
"*.temp", # Generic temp files
|
|
32
|
+
"*.orig", # Merge conflict originals
|
|
33
|
+
"*.pyc", # Python bytecode
|
|
34
|
+
"__pycache__/", # Python cache dir
|
|
35
|
+
".DS_Store", # macOS metadata
|
|
36
|
+
"Thumbs.db", # Windows thumbnail cache
|
|
37
|
+
"*.svg",
|
|
38
|
+
"*.pdf",
|
|
39
|
+
# IDE files
|
|
40
|
+
".idea/", # JetBrains IDEs
|
|
41
|
+
".vscode/", # VS Code
|
|
42
|
+
"*.sublime-*", # Sublime Text
|
|
43
|
+
".project", # Eclipse
|
|
44
|
+
".settings/", # Eclipse
|
|
45
|
+
"*.code-workspace", # VS Code workspace
|
|
46
|
+
# Environment files
|
|
47
|
+
".env", # Environment variables
|
|
48
|
+
".venv/", # Python virtual environments
|
|
49
|
+
"node_modules/", # Node.js dependencies
|
|
50
|
+
"vendor/", # Various dependencies
|
|
51
|
+
# Logs and caches
|
|
52
|
+
"*.log", # Log files
|
|
53
|
+
".cache/", # Cache directories
|
|
54
|
+
".pytest_cache/", # Python test cache
|
|
55
|
+
"coverage/", # Code coverage reports
|
|
56
|
+
] # Always ignore
|
|
57
|
+
for path in gitignore_paths:
|
|
58
|
+
if path.exists():
|
|
59
|
+
with open(path) as f:
|
|
60
|
+
patterns.extend(f.readlines())
|
|
61
|
+
|
|
62
|
+
return PathSpec.from_lines(GitWildMatchPattern, patterns) if patterns else None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class FileWatcher:
|
|
66
|
+
"""Watches source files for changes and AI comments"""
|
|
67
|
+
|
|
68
|
+
# Compiled regex pattern for AI comments
|
|
69
|
+
ai_comment_pattern = re.compile(
|
|
70
|
+
r"(?:#|//|--|;+) *(ai\b.*|ai\b.*|.*\bai[?!]?) *$", re.IGNORECASE
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def __init__(self, coder, gitignores=None, verbose=False, analytics=None, root=None):
|
|
74
|
+
self.coder = coder
|
|
75
|
+
self.io = coder.io
|
|
76
|
+
self.root = Path(root) if root else Path(coder.root)
|
|
77
|
+
self.verbose = verbose
|
|
78
|
+
self.analytics = analytics
|
|
79
|
+
self.stop_event = None
|
|
80
|
+
self.watcher_thread = None
|
|
81
|
+
self.changed_files = set()
|
|
82
|
+
self.gitignores = gitignores
|
|
83
|
+
|
|
84
|
+
self.gitignore_spec = load_gitignores(
|
|
85
|
+
[Path(g) for g in self.gitignores] if self.gitignores else []
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
coder.io.file_watcher = self
|
|
89
|
+
|
|
90
|
+
def filter_func(self, change_type, path):
|
|
91
|
+
"""Filter function for the file watcher"""
|
|
92
|
+
path_obj = Path(path)
|
|
93
|
+
path_abs = path_obj.absolute()
|
|
94
|
+
|
|
95
|
+
if not path_abs.is_relative_to(self.root.absolute()):
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
rel_path = path_abs.relative_to(self.root)
|
|
99
|
+
if self.verbose:
|
|
100
|
+
print("Changed", rel_path)
|
|
101
|
+
|
|
102
|
+
if self.gitignore_spec and self.gitignore_spec.match_file(
|
|
103
|
+
rel_path.as_posix() + ("/" if path_abs.is_dir() else "")
|
|
104
|
+
):
|
|
105
|
+
return False
|
|
106
|
+
|
|
107
|
+
# Check file size before reading content
|
|
108
|
+
if path_abs.is_file() and path_abs.stat().st_size > 1 * 1024 * 1024: # 1MB limit
|
|
109
|
+
return False
|
|
110
|
+
|
|
111
|
+
if self.verbose:
|
|
112
|
+
print("Checking", rel_path)
|
|
113
|
+
|
|
114
|
+
# Check if file contains AI markers
|
|
115
|
+
try:
|
|
116
|
+
comments, _, _ = self.get_ai_comments(str(path_abs))
|
|
117
|
+
return bool(comments)
|
|
118
|
+
except Exception:
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
def get_roots_to_watch(self):
|
|
122
|
+
"""Determine which root paths to watch based on gitignore rules"""
|
|
123
|
+
if self.gitignore_spec:
|
|
124
|
+
roots = [
|
|
125
|
+
str(path)
|
|
126
|
+
for path in self.root.iterdir()
|
|
127
|
+
if not self.gitignore_spec.match_file(
|
|
128
|
+
path.relative_to(self.root).as_posix() + ("/" if path.is_dir() else "")
|
|
129
|
+
)
|
|
130
|
+
]
|
|
131
|
+
# Fallback to watching root if all top-level items are filtered out
|
|
132
|
+
return roots if roots else [str(self.root)]
|
|
133
|
+
return [str(self.root)]
|
|
134
|
+
|
|
135
|
+
def handle_changes(self, changes):
|
|
136
|
+
"""Process the detected changes and update state"""
|
|
137
|
+
if not changes:
|
|
138
|
+
return False
|
|
139
|
+
|
|
140
|
+
changed_files = {str(Path(change[1])) for change in changes}
|
|
141
|
+
self.changed_files.update(changed_files)
|
|
142
|
+
self.io.interrupt_input()
|
|
143
|
+
return True
|
|
144
|
+
|
|
145
|
+
def watch_files(self):
|
|
146
|
+
"""Watch for file changes and process them"""
|
|
147
|
+
try:
|
|
148
|
+
roots_to_watch = self.get_roots_to_watch()
|
|
149
|
+
|
|
150
|
+
for changes in watch(
|
|
151
|
+
*roots_to_watch,
|
|
152
|
+
watch_filter=self.filter_func,
|
|
153
|
+
stop_event=self.stop_event,
|
|
154
|
+
ignore_permission_denied=True,
|
|
155
|
+
):
|
|
156
|
+
if self.handle_changes(changes):
|
|
157
|
+
return
|
|
158
|
+
|
|
159
|
+
except Exception as e:
|
|
160
|
+
if self.verbose:
|
|
161
|
+
dump(f"File watcher error: {e}")
|
|
162
|
+
raise e
|
|
163
|
+
|
|
164
|
+
def start(self):
|
|
165
|
+
"""Start watching for file changes"""
|
|
166
|
+
self.stop_event = threading.Event()
|
|
167
|
+
self.changed_files = set()
|
|
168
|
+
|
|
169
|
+
self.watcher_thread = threading.Thread(target=self.watch_files, daemon=True)
|
|
170
|
+
self.watcher_thread.start()
|
|
171
|
+
|
|
172
|
+
def stop(self):
|
|
173
|
+
"""Stop watching for file changes"""
|
|
174
|
+
if self.stop_event:
|
|
175
|
+
self.stop_event.set()
|
|
176
|
+
if self.watcher_thread:
|
|
177
|
+
self.watcher_thread.join()
|
|
178
|
+
self.watcher_thread = None
|
|
179
|
+
self.stop_event = None
|
|
180
|
+
|
|
181
|
+
def process_changes(self):
|
|
182
|
+
"""Get any detected file changes"""
|
|
183
|
+
|
|
184
|
+
has_action = None
|
|
185
|
+
added = False
|
|
186
|
+
for fname in self.changed_files:
|
|
187
|
+
_, _, action = self.get_ai_comments(fname)
|
|
188
|
+
if action in ("!", "?"):
|
|
189
|
+
has_action = action
|
|
190
|
+
|
|
191
|
+
if fname in self.coder.abs_fnames:
|
|
192
|
+
continue
|
|
193
|
+
if self.analytics:
|
|
194
|
+
self.analytics.event("ai-comments file-add")
|
|
195
|
+
self.coder.abs_fnames.add(fname)
|
|
196
|
+
rel_fname = self.coder.get_rel_fname(fname)
|
|
197
|
+
if not added:
|
|
198
|
+
self.io.tool_output()
|
|
199
|
+
added = True
|
|
200
|
+
self.io.tool_output(f"Added {rel_fname} to the chat")
|
|
201
|
+
|
|
202
|
+
if not has_action:
|
|
203
|
+
if added:
|
|
204
|
+
self.io.tool_output(
|
|
205
|
+
"End your comment with AI! to request changes or AI? to ask questions"
|
|
206
|
+
)
|
|
207
|
+
return ""
|
|
208
|
+
|
|
209
|
+
if self.analytics:
|
|
210
|
+
self.analytics.event("ai-comments execute")
|
|
211
|
+
self.io.tool_output("Processing your request...")
|
|
212
|
+
|
|
213
|
+
if has_action == "!":
|
|
214
|
+
res = watch_code_prompt
|
|
215
|
+
elif has_action == "?":
|
|
216
|
+
res = watch_ask_prompt
|
|
217
|
+
|
|
218
|
+
# Refresh all AI comments from tracked files
|
|
219
|
+
for fname in self.coder.abs_fnames:
|
|
220
|
+
line_nums, comments, _action = self.get_ai_comments(fname)
|
|
221
|
+
if not line_nums:
|
|
222
|
+
continue
|
|
223
|
+
|
|
224
|
+
code = self.io.read_text(fname)
|
|
225
|
+
if not code:
|
|
226
|
+
continue
|
|
227
|
+
|
|
228
|
+
rel_fname = self.coder.get_rel_fname(fname)
|
|
229
|
+
res += f"\n{rel_fname}:\n"
|
|
230
|
+
|
|
231
|
+
# Convert comment line numbers to line indices (0-based)
|
|
232
|
+
lois = [ln - 1 for ln, _ in zip(line_nums, comments) if ln > 0]
|
|
233
|
+
|
|
234
|
+
try:
|
|
235
|
+
context = TreeContext(
|
|
236
|
+
rel_fname,
|
|
237
|
+
code,
|
|
238
|
+
color=False,
|
|
239
|
+
line_number=False,
|
|
240
|
+
child_context=False,
|
|
241
|
+
last_line=False,
|
|
242
|
+
margin=0,
|
|
243
|
+
mark_lois=True,
|
|
244
|
+
loi_pad=3,
|
|
245
|
+
show_top_of_file_parent_scope=False,
|
|
246
|
+
)
|
|
247
|
+
context.lines_of_interest = set()
|
|
248
|
+
context.add_lines_of_interest(lois)
|
|
249
|
+
context.add_context()
|
|
250
|
+
res += context.format()
|
|
251
|
+
except ValueError:
|
|
252
|
+
for ln, comment in zip(line_nums, comments):
|
|
253
|
+
res += f" Line {ln}: {comment}\n"
|
|
254
|
+
|
|
255
|
+
return res
|
|
256
|
+
|
|
257
|
+
def get_ai_comments(self, filepath):
|
|
258
|
+
"""Extract AI comment line numbers, comments and action status from a file"""
|
|
259
|
+
line_nums = []
|
|
260
|
+
comments = []
|
|
261
|
+
has_action = None # None, "!" or "?"
|
|
262
|
+
content = self.io.read_text(filepath, silent=True)
|
|
263
|
+
if not content:
|
|
264
|
+
return None, None, None
|
|
265
|
+
|
|
266
|
+
for i, line in enumerate(content.splitlines(), 1):
|
|
267
|
+
if match := self.ai_comment_pattern.search(line):
|
|
268
|
+
comment = match.group(0).strip()
|
|
269
|
+
if comment:
|
|
270
|
+
line_nums.append(i)
|
|
271
|
+
comments.append(comment)
|
|
272
|
+
comment = comment.lower()
|
|
273
|
+
comment = comment.lstrip("/#-;") # Added semicolon for Lisp comments
|
|
274
|
+
comment = comment.strip()
|
|
275
|
+
if comment.startswith("ai!") or comment.endswith("ai!"):
|
|
276
|
+
has_action = "!"
|
|
277
|
+
elif comment.startswith("ai?") or comment.endswith("ai?"):
|
|
278
|
+
has_action = "?"
|
|
279
|
+
if not line_nums:
|
|
280
|
+
return None, None, None
|
|
281
|
+
return line_nums, comments, has_action
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def main():
|
|
285
|
+
"""Example usage of the file watcher"""
|
|
286
|
+
import argparse
|
|
287
|
+
|
|
288
|
+
parser = argparse.ArgumentParser(description="Watch source files for changes")
|
|
289
|
+
parser.add_argument("directory", help="Directory to watch")
|
|
290
|
+
parser.add_argument(
|
|
291
|
+
"--gitignore",
|
|
292
|
+
action="append",
|
|
293
|
+
help="Path to .gitignore file (can be specified multiple times)",
|
|
294
|
+
)
|
|
295
|
+
args = parser.parse_args()
|
|
296
|
+
|
|
297
|
+
directory = args.directory
|
|
298
|
+
print(f"Watching source files in {directory}...")
|
|
299
|
+
|
|
300
|
+
# Example ignore function that ignores files with "test" in the name
|
|
301
|
+
def ignore_test_files(path):
|
|
302
|
+
return "test" in path.name.lower()
|
|
303
|
+
|
|
304
|
+
watcher = FileWatcher(directory, gitignores=args.gitignore)
|
|
305
|
+
try:
|
|
306
|
+
watcher.start()
|
|
307
|
+
while True:
|
|
308
|
+
if changes := watcher.get_changes():
|
|
309
|
+
for file in sorted(changes.keys()):
|
|
310
|
+
print(file)
|
|
311
|
+
watcher.changed_files = None
|
|
312
|
+
except KeyboardInterrupt:
|
|
313
|
+
print("\nStopped watching files")
|
|
314
|
+
watcher.stop()
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
if __name__ == "__main__":
|
|
318
|
+
main()
|
aider/watch_prompts.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
watch_code_prompt = """
|
|
2
|
+
I've written your instructions in comments in the code and marked them with "ai"
|
|
3
|
+
You can see the "AI" comments shown below (marked with █).
|
|
4
|
+
Find them in the code files I've shared with you, and follow their instructions.
|
|
5
|
+
|
|
6
|
+
After completing those instructions, also be sure to remove all the "AI" comments from the code too.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
watch_ask_prompt = """/ask
|
|
10
|
+
Find the "AI" comments below (marked with █) in the code files I've shared with you.
|
|
11
|
+
They contain my questions that I need you to answer and other instructions for you.
|
|
12
|
+
"""
|