ollama_chat 0.0.119 → 0.0.120

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.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGES.md +45 -0
  3. data/README.md +66 -0
  4. data/Rakefile +1 -1
  5. data/lib/ollama_chat/chat.rb +2 -0
  6. data/lib/ollama_chat/kramdown_ansi.rb +15 -0
  7. data/lib/ollama_chat/oc.rb +44 -0
  8. data/lib/ollama_chat/ollama_chat_config/default_config.yml +9 -2
  9. data/lib/ollama_chat/parsing.rb +1 -1
  10. data/lib/ollama_chat/state_selectors.rb +3 -1
  11. data/lib/ollama_chat/tools/directory_structure.rb +11 -5
  12. data/lib/ollama_chat/tools/generate_image.rb +10 -10
  13. data/lib/ollama_chat/tools/patch_file.rb +1 -1
  14. data/lib/ollama_chat/tts.rb +325 -0
  15. data/lib/ollama_chat/utils/analyze_directory.rb +12 -6
  16. data/lib/ollama_chat/utils/audio_player.rb +147 -0
  17. data/lib/ollama_chat/utils/excon_logger.rb +43 -0
  18. data/lib/ollama_chat/utils/ordered_queue.rb +97 -0
  19. data/lib/ollama_chat/utils.rb +3 -0
  20. data/lib/ollama_chat/version.rb +1 -1
  21. data/lib/ollama_chat.rb +1 -0
  22. data/ollama_chat.gemspec +7 -7
  23. data/spec/ollama_chat/history_spec.rb +1 -1
  24. data/spec/ollama_chat/information_spec.rb +1 -1
  25. data/spec/ollama_chat/parsing_spec.rb +13 -0
  26. data/spec/ollama_chat/tool_calling_spec.rb +2 -2
  27. data/spec/ollama_chat/tools/directory_structure_spec.rb +32 -3
  28. data/spec/ollama_chat/tools/patch_file_spec.rb +1 -1
  29. data/spec/ollama_chat/tts_spec.rb +257 -0
  30. data/spec/ollama_chat/utils/analyze_directory_spec.rb +8 -0
  31. data/spec/ollama_chat/utils/audio_player_spec.rb +180 -0
  32. data/spec/ollama_chat/utils/excon_logger_spec.rb +55 -0
  33. data/spec/ollama_chat/utils/ordered_queue_spec.rb +287 -0
  34. metadata +20 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a7b876b681130c1ff2ec672460bb04aa85ed095e9358dbdaa62d744f82d68720
4
- data.tar.gz: 3f85175d15231880d45e8f192f9f9af47c861cb8f1475d2fe653309ca173ab1c
3
+ metadata.gz: cfff20e994aa12d921fd4ce0d050fbaaae9d66ade06fe419d6dbd9ce0dad1a4e
4
+ data.tar.gz: 45f485ac6fc502e6634626d6e891402acf74516491af95a8e26773905924d5a0
5
5
  SHA512:
6
- metadata.gz: 9cf60761b6ce28cb22873b30808d3398ffcefc9e9991fcf781621b4db8a8fed5fd6cca21de046d8bf2851dd60a7529cfa162125ca4ee567e32c09042f4dc4e81
7
- data.tar.gz: a2f1e6210141a8f00c4b9428453c49e4e1673e51521f788dcec694cd2c2c9963542bd491618ec8d1a4bb3ad5788588ca3d2c7f51ed3b67b977eaac1b4a59115a
6
+ metadata.gz: 13a2ca128a857c8980684bf437a5c9470d92b42babed878010db2eaba75de655f5ded18f7baccf3ef27d05191c4edfad7fd1c2a190a3f098ff880e0fefe0606d
7
+ data.tar.gz: c6785ad30f764d56ea19c8bdaaad6dee89385317ecd4a15c6925f12f5637f8a31c4393dcca4143669f2771bd594a9a383ff24e4fdd1e1dcbd070f357128a0446
data/CHANGES.md CHANGED
@@ -1,5 +1,50 @@
1
1
  # Changes
2
2
 
3
+ ## 2026-09-18 v0.0.120
4
+
5
+ * **TTS / audio.cpp integration**
6
+ * Add `OllamaChat::TTS` with parallel block synthesis via worker threads,
7
+ ordered playback through `OrderedQueue`, RIFF-header stripping,
8
+ config-driven streaming, and `join_workers` for test teardown.
9
+ * Add `OllamaChat::Utils::AudioPlayer` (PCM pipe to `ffplay` with silence
10
+ injection to prevent EOF hangs), `OllamaChat::Utils::OrderedQueue`
11
+ (thread-safe min-heap using `Mutex` + `ConditionVariable`), and
12
+ `OllamaChat::Utils::ExconLogger` (routes `Excon` log calls through
13
+ `chat.log(:debug)`).
14
+ * Add `KramdownANSI#kramdown_markdown_remove` to strip markdown to plain
15
+ text for TTS playback.
16
+ * Add `TTS_URL` (required, sensitive, URI-decoded) and
17
+ `AUDIO_PLAYER_CONFIG` (JSON: `command`, `frequency`, `bits`, `pause`)
18
+ to `OC`, enabling `ConstConf::JSONPlugin`.
19
+ * Default `voice.handler` to `OllamaChat::TTS` in `default_config.yml`;
20
+ add `model: 'chatterbox'` and `stream: { enabled: false, format:
21
+ 'audio' }`.
22
+ * Simplify `state_selectors.rb` to a single
23
+ `ask_and_send(:voices, model:)` call with `Array()` guard.
24
+ * Switch `generate_image.rb` to `Excon.new(url, logger:)` with
25
+ `ExconLogger`.
26
+ * Add specs: `tts_spec.rb`, `audio_player_spec.rb`,
27
+ `excon_logger_spec.rb`, `ordered_queue_spec.rb`; migrate existing
28
+ specs from `chat.send` to `chat.expose`.
29
+
30
+ * **`include_hidden` in `directory_structure`**
31
+ * Add `include_hidden:` keyword argument to `generate_structure` and
32
+ `recurse_generate_structure` in `analyze_directory.rb` with YARD docs;
33
+ fix the recursive call to forward the flag so dotfiles are honored at
34
+ all depths.
35
+ * Add an `include_hidden` boolean parameter to the `directory_structure`
36
+ tool definition and pass-through in `execute`.
37
+ * `parsing.rb` now passes `include_hidden: true` for all directory
38
+ references.
39
+ * Add `include_hidden` tests to `analyze_directory_spec.rb`,
40
+ `directory_structure_spec.rb`, and `parsing_spec.rb`.
41
+
42
+ * **Housekeeping**
43
+ * Bump `tins` to **~> 1.57** in `Rakefile` and gemspec (kwarg support for
44
+ `ask_and_send`).
45
+ * Add an External Dependencies section to `README.md` with per-executable
46
+ tables, install instructions, and `TTS_URL` documentation.
47
+
3
48
  ## 2026-09-14 v0.0.119
4
49
 
5
50
  * **Word-boundary fuzzy matching in chooser**
data/README.md CHANGED
@@ -62,6 +62,7 @@ The following environment variables can be used to configure behavior:
62
62
  - `OLLAMA_URL` - Base URL for Ollama server (default: `http://localhost:11434`)
63
63
  - `OLLAMA_HOST` - Base hostname for Ollama server (default: `localhost:11434`)
64
64
  - `OLLAMA_SEARXNG_URL` - SearxNG search endpoint URL
65
+ - `TTS_URL` - Base URL for the [audio.cpp](https://github.com/0xshug0/audio.cpp) TTS server (default: `http://localhost:8880`)
65
66
 
66
67
  #### Chat Settings
67
68
  - `OLLAMA_CHAT_MODEL` - Default model to use (e.g., `llama3.1`)
@@ -479,6 +480,71 @@ low-token semantic discovery and `file_context` for a comprehensive view of
479
480
  modules or patterns. Be cautious with broad patterns in `file_context`, as
480
481
  importing too many files can exceed the LLM's context window.*
481
482
 
483
+ ## External Dependencies
484
+
485
+ **ollama_chat** invokes several external executables at runtime. Most are
486
+ optional and only required for specific features; the app degrades gracefully
487
+ when they are absent.
488
+
489
+ ### Always Available
490
+
491
+ | Executable | Purpose |
492
+ | :--- | :--- |
493
+ | `sh` | Shell executor tool (`execute_shell`) |
494
+ | `which` | Detecting optional executables on `$PATH` |
495
+ | `git` | Runtime info (branch, SHA, remote origin) |
496
+
497
+ ### Editor & Diff
498
+
499
+ | Executable | Purpose | Fallback |
500
+ | :--- | :--- | :--- |
501
+ | `vim` | Editor integration (`/vim`, `open_file_in_editor`) | `vi` |
502
+ | `vimdiff` | Interactive diff (`patch_file`, `/config diff`) | `$DIFF_TOOL` env var |
503
+ | `diff` | Unified diff generation | `$DIFF_COMMAND` env var |
504
+ | `less` / `more` | Pager fallback | `$PAGER` env var |
505
+
506
+ ### Optional Feature Dependencies
507
+
508
+ | Executable | Feature | Notes |
509
+ | :--- | :--- | :--- |
510
+ | `ffplay` | TTS audio playback | Part of **FFmpeg**; required only when `voice.handler` is `OllamaChat::TTS`; the TTS backend is [audio.cpp](https://github.com/0xshug0/audio.cpp) |
511
+ | `say` | macOS system TTS | Required only when `voice.handler` is `OllamaChat::Say` (macOS only) |
512
+ | `ctags` | Symbol resolution | Part of **Exuberant Ctags** or **Universal Ctags**; required for `resolve_tag` tool |
513
+ | `twg` | Jira / Teamwork Graph CLI | Required for `execute_jira_twg` and `get_jira_issue` tools |
514
+ | `pandoc` | EPUB document parsing | Part of **Pandoc**; used when importing `.epub` files |
515
+ | `gs` | PDF compression | Part of **Ghostscript**; used when parsing PDFs |
516
+ | `ctc` / `pbcopy` / `wl-copy` | Clipboard copy | `ctc` (default), `pbcopy` (macOS), `wl-copy` (Wayland); configurable via `copy:` in `config.yml` |
517
+ | `pfc` / `pbpaste` / `wl-paste` | Clipboard paste | `pfc` (default), `pbpaste` (macOS), `wl-paste` (Wayland); configurable via `paste:` in `config.yml` |
518
+ | `open` / `xdg-open` | Open URLs in browser | macOS / Linux respectively; used by `browse` tool |
519
+ | `rspec` | Test runner | Required for `run_tests` tool (configurable via `$OLLAMA_CHAT_TOOLS_TEST_RUNNER`) |
520
+ | `docker` | Ruby sandbox | Required for `eval_ruby` tool |
521
+ | `ruby` | Syntax checker | Used by `syntax_checkers.ruby` (`ruby -wc`) |
522
+ | `node` | Syntax checker | Used by `syntax_checkers.node` (`node --check`) |
523
+ | `python3` | Syntax checker | Used by `syntax_checkers.python` (`python3 -m py_compile`) |
524
+ | `bash` | Syntax checker | Used by `syntax_checkers.bash` (`bash -n`) |
525
+
526
+ ### Example Installation (Debian / Ubuntu)
527
+
528
+ ```bash
529
+ # Core (usually already present)
530
+ sudo apt install git vim vimdiff diff
531
+
532
+ # TTS (audio.cpp handler)
533
+ sudo apt install ffmpeg
534
+
535
+ # Clipboard
536
+ # ctc is a small helper; see the project README or install from source
537
+
538
+ # Document parsing
539
+ sudo apt install pandoc ghostscript
540
+
541
+ # Symbol resolution
542
+ sudo apt install universal-ctags
543
+
544
+ # eval_ruby
545
+ sudo apt install docker.io
546
+ ```
547
+
482
548
  ## Download
483
549
 
484
550
  The homepage of this app is located at
data/Rakefile CHANGED
@@ -49,7 +49,7 @@ GemHadar do
49
49
  dependency 'reverse_markdown', '~> 3.0'
50
50
  dependency 'kramdown-ansi', '~> 0.5'
51
51
  dependency 'complex_config', '~> 0.22', '>= 0.22.2'
52
- dependency 'tins', '~> 1.55'
52
+ dependency 'tins', '~> 1.57'
53
53
  dependency 'search_ui', '>= 0.2'
54
54
  dependency 'amatch', '~> 0.4'
55
55
  dependency 'pdf-reader', '~> 2.0'
@@ -172,6 +172,8 @@ class OllamaChat::Chat
172
172
  def voice_handler
173
173
  defined?(RSpec) and return []
174
174
  case config.voice.handler
175
+ when 'OllamaChat::TTS'
176
+ OllamaChat::TTS
175
177
  when 'OllamaChat::Say'
176
178
  OllamaChat::Say
177
179
  end
@@ -48,4 +48,19 @@ module OllamaChat::KramdownANSI
48
48
  content = content.encode(invalid: :replace)
49
49
  retry
50
50
  end
51
+
52
+ # The kramdown_markdown_remove method strips all markdown formatting from
53
+ # content by first rendering it to ANSI and then removing the escape
54
+ # sequences, yielding plain text suitable for speech synthesis.
55
+ #
56
+ # @param content [ String, nil ] the markdown content to strip.
57
+ # If nil, returns an empty string.
58
+ #
59
+ # @return [ String ] the content with all markdown and ANSI
60
+ # formatting removed, suitable for TTS playback
61
+ def kramdown_markdown_remove(content)
62
+ content = kramdown_ansi_parse(content)
63
+ content.empty? and return ''
64
+ OllamaChat::Utils::StripANSI.strip_ansi(content)
65
+ end
51
66
  end
@@ -14,6 +14,8 @@ require 'shellwords'
14
14
  module OC
15
15
  include ConstConf
16
16
 
17
+ plugin ConstConf::JSONPlugin
18
+
17
19
  description 'Environment config for OllamaChat'
18
20
  prefix ''
19
21
 
@@ -183,6 +185,48 @@ module OC
183
185
  default { ENV['USER'] }
184
186
  end
185
187
 
188
+ TTS_URL = set do
189
+ description 'Base URL for the TTS (Text-to-Speech) service'
190
+ default 'http://localhost:8880'
191
+ required true
192
+ sensitive true
193
+ decode { URI.parse(_1) if _1.present? }
194
+ check { value.scheme =~ /\Ahttps?\z/ }
195
+ end
196
+
197
+ AUDIO_PLAYER_CONFIG = set do
198
+ description <<~EOT
199
+ Audio player configuration (JSON with four keys):
200
+
201
+ command – Shell command that receives raw PCM on stdin.
202
+ Opened via IO.popen(cmd, "w"); the playback thread
203
+ writes audio bytes and silence chunks to its stdin.
204
+ Default: ffplay s16le 24 kHz mono pipe.
205
+
206
+ frequency – Sample rate in Hz (e.g. 24000). Used to compute
207
+ the byte size of silence chunks: frequency ×
208
+ (bits / 8) × pause.
209
+
210
+ bits – Bit depth per sample (e.g. 16). Determines bytes
211
+ per sample for silence chunk calculation.
212
+
213
+ pause – Seconds of silence injected when the audio queue
214
+ is empty. Prevents the playback process from
215
+ seeing EOF and hanging. Also the sleep duration
216
+ between silence writes.
217
+ EOT
218
+ default <<~EOT
219
+ {
220
+ "command": "ffplay -autoexit -nodisp -loglevel quiet -vn -volume 80 -f s16le -ar 24000 -ch_layout mono -i -",
221
+ "frequency": 24000,
222
+ "bits": 16,
223
+ "pause": 0.01
224
+ }
225
+ EOT
226
+ decode json
227
+ check { value.command && value.frequency && value.bits && value.pause if value.present? }
228
+ end
229
+
186
230
  module TOOLS
187
231
  description 'Tool specific configuration settings'
188
232
 
@@ -445,9 +445,16 @@ prompts:
445
445
  no reference to yourself or the act of
446
446
  writing.
447
447
  voice:
448
- handler: OllamaChat::Say
448
+ #handler: OllamaChat::Say
449
+ #enabled: false
450
+ #default: Samantha
451
+ handler: OllamaChat::TTS
449
452
  enabled: false
450
- default: Samantha
453
+ default: miyu
454
+ model: 'chatterbox'
455
+ stream:
456
+ enabled: false # false for offline models, true for streaming
457
+ format: 'audio'
451
458
  roles:
452
459
  user: 👨 %{sender_name}
453
460
  assistant: 🤖 %{sender_name}
@@ -280,7 +280,7 @@ module OllamaChat::Parsing
280
280
  content.scan(CONTENT_REGEXP).each { |url, file_url, quoted_file, file|
281
281
  if file && Pathname.new(file).expand_path.directory?
282
282
  max_depth = config.parsing.directory_structure_max_depth
283
- contents << generate_structure(file, max_depth:).to_json
283
+ contents << generate_structure(file, max_depth:, include_hidden: true).to_json
284
284
  next
285
285
  end
286
286
  check_exist = false
@@ -269,7 +269,9 @@ module OllamaChat::StateSelectors
269
269
  states: OllamaChat::ThinkControl::THINK_MODE_STATES,
270
270
  off: OllamaChat::ThinkControl::THINK_MODE_STATES[0, 1],
271
271
  )
272
- list = voice_handler.ask_and_send(:voices) || []
272
+ list = Array(
273
+ voice_handler.ask_and_send(:voices, model: config.voice.model?)
274
+ )
273
275
  @voices = DatabaseStateSelector.new(
274
276
  chat: self,
275
277
  attribute: :current_voice,
@@ -55,6 +55,10 @@ class OllamaChat::Tools::DirectoryStructure
55
55
  unlimited (defaults to nil)
56
56
  EOT
57
57
  ),
58
+ include_hidden: Tool::Function::Parameters::Property.new(
59
+ type: 'boolean',
60
+ description: 'Include hidden files and directories (dotfiles), (default: false)'
61
+ ),
58
62
  },
59
63
  required: []
60
64
  )
@@ -75,16 +79,18 @@ class OllamaChat::Tools::DirectoryStructure
75
79
  # @raise [StandardError] if there's an issue with directory traversal or JSON
76
80
  # serialization
77
81
  def execute(tool_call, **opts)
78
- chat = opts[:chat]
79
- config = chat.config
80
- path = Pathname.new(tool_call.function.arguments.path || '.')
81
- suffix = tool_call.function.arguments.suffix.full?
82
- max_depth = tool_call.function.arguments.max_depth.full?
82
+ chat = opts[:chat]
83
+ config = chat.config
84
+ path = Pathname.new(tool_call.function.arguments.path || '.')
85
+ suffix = tool_call.function.arguments.suffix.full?
86
+ max_depth = tool_call.function.arguments.max_depth.full?
87
+ include_hidden = tool_call.function.arguments.include_hidden
83
88
 
84
89
  structure = generate_structure(
85
90
  path,
86
91
  max_depth:,
87
92
  suffix:,
93
+ include_hidden:,
88
94
  exclude: config.tools.functions.directory_structure.exclude?,
89
95
  )
90
96
  structure.to_json
@@ -77,13 +77,13 @@ class OllamaChat::Tools::GenerateImage
77
77
  url = service_url + '/prompt'
78
78
  payload = { prompt: workflow }
79
79
  started = Time.now
80
- prompt_id = post_url(url, payload).prompt_id
80
+ prompt_id = post_url(url, payload, chat).prompt_id
81
81
 
82
82
  prompt_id.nil? and raise OllamaChat::OllamaChatError,
83
83
  "failed to trigger ComfyUI with #{prompt_id}"
84
84
 
85
85
  # 3. Poll for completion
86
- filename = poll_for_image(service_url, prompt_id, config)
86
+ filename = poll_for_image(service_url, prompt_id, config, chat)
87
87
 
88
88
  filename.nil? and raise OllamaChat::OllamaChatError,
89
89
  'Image generation took too long or failed'
@@ -122,9 +122,9 @@ class OllamaChat::Tools::GenerateImage
122
122
  # @param url [URI] the target URL
123
123
  # @param payload [Hash] the data to be sent as JSON
124
124
  # @return [JSON::GenericObject] the parsed JSON response
125
- def post_url(url, payload)
126
- response = Excon.post(
127
- url,
125
+ def post_url(url, payload, chat)
126
+ logger = OllamaChat::Utils::ExconLogger.new(chat)
127
+ response = Excon.new(url, logger:).post(
128
128
  body: JSON.dump(payload),
129
129
  headers: { 'Content-Type' => 'application/json' },
130
130
  expects: 200
@@ -136,9 +136,9 @@ class OllamaChat::Tools::GenerateImage
136
136
  #
137
137
  # @param url [URI] the target URL
138
138
  # @return [JSON::GenericObject] the parsed JSON response
139
- def get_url(url)
140
- response = Excon.get(
141
- url,
139
+ def get_url(url, chat)
140
+ logger = OllamaChat::Utils::ExconLogger.new(chat)
141
+ response = Excon.new(url, logger:).get(
142
142
  headers: { 'Accept' => 'application/json' },
143
143
  expects: 200
144
144
  )
@@ -151,7 +151,7 @@ class OllamaChat::Tools::GenerateImage
151
151
  # @param prompt_id [String] the ID of the prompt to track
152
152
  # @param config [ComplexConfig::Settings] the configuration settings for timeout
153
153
  # @return [String, nil] the filename of the generated image, or nil if it timed out
154
- def poll_for_image(service_url, prompt_id, config)
154
+ def poll_for_image(service_url, prompt_id, config, chat)
155
155
  history_url = service_url + '/history'
156
156
  filename = nil
157
157
 
@@ -159,7 +159,7 @@ class OllamaChat::Tools::GenerateImage
159
159
  sleep = -(config.tools.functions.generate_image.timeout_duration? || 60)
160
160
 
161
161
  attempt attempts:, sleep:, exception_class: nil do
162
- response = get_url(history_url)
162
+ response = get_url(history_url, chat)
163
163
  output_data = response[prompt_id]
164
164
 
165
165
  if filename = output_data&.outputs&.each_pair&.first&.last&.images&.first&.filename
@@ -190,7 +190,7 @@ class OllamaChat::Tools::PatchFile
190
190
  message: ,
191
191
  mtime: ,
192
192
  checksum: ,
193
- syntax_check: syntax_check,
193
+ syntax_check: ,
194
194
  }).compact.to_json
195
195
 
196
196
  rescue => e