chatterer 0.1.18__py3-none-any.whl → 0.1.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.
Files changed (44) hide show
  1. chatterer/__init__.py +93 -93
  2. chatterer/common_types/__init__.py +21 -21
  3. chatterer/common_types/io.py +19 -19
  4. chatterer/examples/__init__.py +0 -0
  5. chatterer/examples/anything_to_markdown.py +85 -91
  6. chatterer/examples/get_code_snippets.py +55 -62
  7. chatterer/examples/login_with_playwright.py +156 -167
  8. chatterer/examples/make_ppt.py +488 -497
  9. chatterer/examples/pdf_to_markdown.py +100 -107
  10. chatterer/examples/pdf_to_text.py +54 -56
  11. chatterer/examples/transcription_api.py +112 -123
  12. chatterer/examples/upstage_parser.py +89 -100
  13. chatterer/examples/webpage_to_markdown.py +70 -79
  14. chatterer/interactive.py +354 -354
  15. chatterer/language_model.py +533 -533
  16. chatterer/messages.py +21 -21
  17. chatterer/strategies/__init__.py +13 -13
  18. chatterer/strategies/atom_of_thoughts.py +975 -975
  19. chatterer/strategies/base.py +14 -14
  20. chatterer/tools/__init__.py +46 -46
  21. chatterer/tools/caption_markdown_images.py +384 -384
  22. chatterer/tools/citation_chunking/__init__.py +3 -3
  23. chatterer/tools/citation_chunking/chunks.py +53 -53
  24. chatterer/tools/citation_chunking/citation_chunker.py +118 -118
  25. chatterer/tools/citation_chunking/citations.py +285 -285
  26. chatterer/tools/citation_chunking/prompt.py +157 -157
  27. chatterer/tools/citation_chunking/reference.py +26 -26
  28. chatterer/tools/citation_chunking/utils.py +138 -138
  29. chatterer/tools/convert_pdf_to_markdown.py +393 -302
  30. chatterer/tools/convert_to_text.py +446 -447
  31. chatterer/tools/upstage_document_parser.py +705 -705
  32. chatterer/tools/webpage_to_markdown.py +739 -739
  33. chatterer/tools/youtube.py +146 -146
  34. chatterer/utils/__init__.py +15 -15
  35. chatterer/utils/base64_image.py +285 -285
  36. chatterer/utils/bytesio.py +59 -59
  37. chatterer/utils/code_agent.py +237 -237
  38. chatterer/utils/imghdr.py +148 -148
  39. {chatterer-0.1.18.dist-info → chatterer-0.1.20.dist-info}/METADATA +392 -392
  40. chatterer-0.1.20.dist-info/RECORD +44 -0
  41. {chatterer-0.1.18.dist-info → chatterer-0.1.20.dist-info}/WHEEL +1 -1
  42. chatterer-0.1.20.dist-info/entry_points.txt +10 -0
  43. chatterer-0.1.18.dist-info/RECORD +0 -42
  44. {chatterer-0.1.18.dist-info → chatterer-0.1.20.dist-info}/top_level.txt +0 -0
@@ -1,107 +1,100 @@
1
- def resolve_import_path_and_get_logger():
2
- # ruff: noqa: E402
3
- import logging
4
- import sys
5
-
6
- if __name__ == "__main__" and "." not in sys.path:
7
- sys.path.append(".")
8
-
9
- logger = logging.getLogger(__name__)
10
- return logger
11
-
12
-
13
- logger = resolve_import_path_and_get_logger()
14
- import sys
15
- from pathlib import Path
16
- from typing import Optional
17
-
18
- from spargear import ArgumentSpec, BaseArguments
19
-
20
- from chatterer import Chatterer, PdfToMarkdown
21
-
22
-
23
- class PdfToMarkdownArgs(BaseArguments):
24
- in_path: ArgumentSpec[str] = ArgumentSpec(
25
- ["in-path"], help="Path to the input PDF file or a directory containing PDF files."
26
- )
27
- out_path: Optional[str] = None
28
- """Output path. For a file, path to the output markdown file. For a directory, output directory for .md files."""
29
- chatterer: ArgumentSpec[Chatterer] = ArgumentSpec(
30
- ["--chatterer"],
31
- default=None,
32
- help="Chatterer instance for communication.",
33
- type=Chatterer.from_provider,
34
- required=True,
35
- )
36
- pages: Optional[str] = None
37
- """Page indices to convert (e.g., '1,3,5-9')."""
38
- recursive: bool = False
39
- """If input is a directory, search for PDFs recursively."""
40
-
41
- def run(self) -> list[dict[str, str]]:
42
- in_path = Path(self.in_path.unwrap()).resolve()
43
- page_indices = parse_page_indices(self.pages) if self.pages else None
44
- pdf_files: list[Path] = []
45
- is_dir = False
46
- if in_path.is_file():
47
- if in_path.suffix.lower() != ".pdf":
48
- sys.exit(1)
49
- pdf_files.append(in_path)
50
- elif in_path.is_dir():
51
- is_dir = True
52
- pattern = "*.pdf"
53
- pdf_files = sorted([
54
- f for f in (in_path.rglob(pattern) if self.recursive else in_path.glob(pattern)) if f.is_file()
55
- ])
56
- if not pdf_files:
57
- sys.exit(0)
58
- else:
59
- sys.exit(1)
60
- if self.out_path:
61
- out_base = Path(self.out_path).resolve()
62
- elif is_dir:
63
- out_base = in_path
64
- else:
65
- out_base = in_path.with_suffix(".md")
66
-
67
- if is_dir:
68
- out_base.mkdir(parents=True, exist_ok=True)
69
- else:
70
- out_base.parent.mkdir(parents=True, exist_ok=True)
71
-
72
- converter = PdfToMarkdown(chatterer=self.chatterer.unwrap())
73
- results: list[dict[str, str]] = []
74
- for pdf in pdf_files:
75
- out_path = (out_base / (pdf.stem + ".md")) if is_dir else out_base
76
- md = converter.convert(str(pdf), page_indices)
77
- out_path.parent.mkdir(parents=True, exist_ok=True)
78
- out_path.write_text(md, encoding="utf-8")
79
- results.append({"input": pdf.as_posix(), "output": out_path.as_posix(), "result": md})
80
- logger.info(f"Converted {len(pdf_files)} PDF(s) to markdown and saved to `{out_base}`.")
81
- return results
82
-
83
-
84
- def parse_page_indices(pages_str: str) -> list[int] | None:
85
- if not pages_str:
86
- return None
87
- indices: set[int] = set()
88
- for part in pages_str.split(","):
89
- part = part.strip()
90
- if not part:
91
- continue
92
- if "-" in part:
93
- start_str, end_str = part.split("-", 1)
94
- start = int(start_str.strip())
95
- end = int(end_str.strip())
96
- if start > end:
97
- raise ValueError
98
- indices.update(range(start, end + 1))
99
- else:
100
- indices.add(int(part))
101
- if not indices:
102
- raise ValueError
103
- return sorted(indices)
104
-
105
-
106
- if __name__ == "__main__":
107
- PdfToMarkdownArgs().run()
1
+ import logging
2
+ import sys
3
+ from pathlib import Path
4
+ from typing import Optional
5
+
6
+ from spargear import ArgumentSpec, BaseArguments
7
+
8
+ from chatterer import Chatterer, PdfToMarkdown
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class PdfToMarkdownArgs(BaseArguments):
14
+ input: str
15
+ """Input PDF file or directory containing PDF files to convert to markdown."""
16
+ output: Optional[str] = None
17
+ """Output path. For a file, path to the output markdown file. For a directory, output directory for .md files."""
18
+ """Chatterer instance for communication."""
19
+ pages: Optional[str] = None
20
+ """Page indices to convert (e.g., '1,3,5-9')."""
21
+ recursive: bool = False
22
+ """If input is a directory, search for PDFs recursively."""
23
+ chatterer: ArgumentSpec[Chatterer] = ArgumentSpec(
24
+ ["--chatterer"],
25
+ default_factory=lambda: Chatterer.from_provider("google:gemini-2.5-flash-preview-05-20"),
26
+ help="Chatterer instance for communication.",
27
+ type=Chatterer.from_provider,
28
+ )
29
+
30
+ def run(self) -> list[dict[str, str]]:
31
+ input = Path(self.input).resolve()
32
+ page_indices = parse_page_indices(self.pages) if self.pages else None
33
+ pdf_files: list[Path] = []
34
+ is_dir = False
35
+ if input.is_file():
36
+ if input.suffix.lower() != ".pdf":
37
+ sys.exit(1)
38
+ pdf_files.append(input)
39
+ elif input.is_dir():
40
+ is_dir = True
41
+ pattern = "*.pdf"
42
+ pdf_files = sorted([
43
+ f for f in (input.rglob(pattern) if self.recursive else input.glob(pattern)) if f.is_file()
44
+ ])
45
+ if not pdf_files:
46
+ sys.exit(0)
47
+ else:
48
+ sys.exit(1)
49
+ if self.output:
50
+ out_base = Path(self.output).resolve()
51
+ elif is_dir:
52
+ out_base = input
53
+ else:
54
+ out_base = input.with_suffix(".md")
55
+
56
+ if is_dir:
57
+ out_base.mkdir(parents=True, exist_ok=True)
58
+ else:
59
+ out_base.parent.mkdir(parents=True, exist_ok=True)
60
+
61
+ converter = PdfToMarkdown(chatterer=self.chatterer.unwrap())
62
+ results: list[dict[str, str]] = []
63
+ for pdf in pdf_files:
64
+ output = (out_base / (pdf.stem + ".md")) if is_dir else out_base
65
+ md = converter.convert(str(pdf), page_indices)
66
+ output.parent.mkdir(parents=True, exist_ok=True)
67
+ output.write_text(md, encoding="utf-8")
68
+ results.append({"input": pdf.as_posix(), "output": output.as_posix(), "result": md})
69
+ logger.info(f"Converted {len(pdf_files)} PDF(s) to markdown and saved to `{out_base}`.")
70
+ return results
71
+
72
+
73
+ def parse_page_indices(pages_str: str) -> list[int] | None:
74
+ if not pages_str:
75
+ return None
76
+ indices: set[int] = set()
77
+ for part in pages_str.split(","):
78
+ part = part.strip()
79
+ if not part:
80
+ continue
81
+ if "-" in part:
82
+ start_str, end_str = part.split("-", 1)
83
+ start = int(start_str.strip())
84
+ end = int(end_str.strip())
85
+ if start > end:
86
+ raise ValueError
87
+ indices.update(range(start, end + 1))
88
+ else:
89
+ indices.add(int(part))
90
+ if not indices:
91
+ raise ValueError
92
+ return sorted(indices)
93
+
94
+
95
+ def main() -> None:
96
+ PdfToMarkdownArgs().run()
97
+
98
+
99
+ if __name__ == "__main__":
100
+ main()
@@ -1,56 +1,54 @@
1
- def resolve_import_path_and_get_logger():
2
- # ruff: noqa: E402
3
- import logging
4
- import sys
5
-
6
- if __name__ == "__main__" and "." not in sys.path:
7
- sys.path.append(".")
8
-
9
- logger = logging.getLogger(__name__)
10
- return logger
11
-
12
-
13
- logger = resolve_import_path_and_get_logger()
14
- import sys
15
- from pathlib import Path
16
-
17
- from spargear import ArgumentSpec, BaseArguments
18
-
19
- from chatterer.tools.convert_to_text import pdf_to_text
20
-
21
-
22
- class PdfToTextArgs(BaseArguments):
23
- in_path: ArgumentSpec[Path] = ArgumentSpec(["in-path"], help="Path to the PDF file.")
24
- out_path: ArgumentSpec[Path] = ArgumentSpec(["--out-path"], default=None, help="Output file path.")
25
- pages: ArgumentSpec[str] = ArgumentSpec(["--pages"], default=None, help="Page indices to extract, e.g. '1,3,5-9'.")
26
-
27
- def run(self) -> None:
28
- input = self.in_path.unwrap().resolve()
29
- out = self.out_path.value or input.with_suffix(".txt")
30
- if not input.is_file():
31
- sys.exit(1)
32
- out.write_text(
33
- pdf_to_text(input, parse_page_indices(pages_arg) if (pages_arg := self.pages.value) else None),
34
- encoding="utf-8",
35
- )
36
- logger.info(f"Extracted text from `{input}` to `{out}`")
37
-
38
-
39
- def parse_page_indices(pages_str: str) -> list[int]:
40
- indices: set[int] = set()
41
- for part in pages_str.split(","):
42
- part = part.strip()
43
- if "-" in part:
44
- start_str, end_str = part.split("-", 1)
45
- start = int(start_str)
46
- end = int(end_str)
47
- if start > end:
48
- raise ValueError
49
- indices.update(range(start, end + 1))
50
- else:
51
- indices.add(int(part))
52
- return sorted(indices)
53
-
54
-
55
- if __name__ == "__main__":
56
- PdfToTextArgs().run()
1
+ import logging
2
+ import sys
3
+ from pathlib import Path
4
+ from typing import Optional
5
+
6
+ from spargear import BaseArguments
7
+
8
+ from chatterer.tools.convert_to_text import pdf_to_text
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class PdfToTextArgs(BaseArguments):
14
+ input: Path
15
+ """Path to the PDF file to convert to text."""
16
+ output: Optional[Path]
17
+ """Path to the output text file. If not provided, defaults to the input file with a .txt suffix."""
18
+ pages: Optional[str] = None
19
+ """Comma-separated list of page indices to extract from the PDF. Supports ranges, e.g., '1,3,5-9'."""
20
+
21
+ def run(self) -> None:
22
+ input = self.input.resolve()
23
+ out = self.output or input.with_suffix(".txt")
24
+ if not input.is_file():
25
+ sys.exit(1)
26
+ out.write_text(
27
+ pdf_to_text(path_or_file=input, page_indices=self.pages),
28
+ encoding="utf-8",
29
+ )
30
+ logger.info(f"Extracted text from `{input}` to `{out}`")
31
+
32
+
33
+ def parse_page_indices(pages_str: str) -> list[int]:
34
+ indices: set[int] = set()
35
+ for part in pages_str.split(","):
36
+ part = part.strip()
37
+ if "-" in part:
38
+ start_str, end_str = part.split("-", 1)
39
+ start = int(start_str)
40
+ end = int(end_str)
41
+ if start > end:
42
+ raise ValueError
43
+ indices.update(range(start, end + 1))
44
+ else:
45
+ indices.add(int(part))
46
+ return sorted(indices)
47
+
48
+
49
+ def main() -> None:
50
+ PdfToTextArgs().run()
51
+
52
+
53
+ if __name__ == "__main__":
54
+ main()
@@ -1,123 +1,112 @@
1
- # pyright: reportUnknownVariableType=false, reportUnknownMemberType=false, reportArgumentType=false, reportMissingTypeStubs=false
2
-
3
- from io import BytesIO
4
- from pathlib import Path
5
- from typing import cast
6
-
7
- from openai import OpenAI
8
- from pydub import AudioSegment
9
- from spargear import ArgumentSpec, BaseArguments
10
-
11
- # Maximum chunk length in seconds
12
- MAX_CHUNK_DURATION = 600
13
-
14
-
15
- class TranscriptionApiArguments(BaseArguments):
16
- in_path = ArgumentSpec(
17
- ["in-path"],
18
- type=Path,
19
- help="The audio file to transcribe.",
20
- )
21
- out_path = ArgumentSpec(
22
- ["--out-path"],
23
- type=Path,
24
- default=None,
25
- help="Path to save the transcription output.",
26
- )
27
- model: ArgumentSpec[str] = ArgumentSpec(
28
- ["--model"],
29
- default="gpt-4o-transcribe",
30
- help="The model to use for transcription.",
31
- )
32
- api_key: ArgumentSpec[str] = ArgumentSpec(
33
- ["--api-key"],
34
- default=None,
35
- help="The API key for authentication.",
36
- )
37
- base_url: ArgumentSpec[str] = ArgumentSpec(
38
- ["--base-url"],
39
- default="https://api.openai.com/v1",
40
- help="The base URL for the API.",
41
- )
42
-
43
- def run(self) -> None:
44
- audio_path = self.in_path.unwrap()
45
- model = self.model.unwrap()
46
-
47
- client = OpenAI(api_key=self.api_key.value, base_url=self.base_url.value)
48
-
49
- audio = load_audio_segment(audio_path)
50
-
51
- segments = split_audio(audio, MAX_CHUNK_DURATION)
52
- print(f"[i] Audio duration: {len(audio) / 1000:.1f}s; splitting into {len(segments)} segment(s)")
53
-
54
- transcripts: list[str] = []
55
- for idx, seg in enumerate(segments, start=1):
56
- print(f"[i] Transcribing segment {idx}/{len(segments)}...")
57
- transcripts.append(transcribe_segment(seg, client, model))
58
-
59
- full_transcript = "\n\n".join(transcripts)
60
- output_path: Path = self.out_path.value or audio_path.with_suffix(".txt")
61
- output_path.write_text(full_transcript, encoding="utf-8")
62
- print(f"[✓] Transcription saved to: {output_path}")
63
-
64
-
65
- def load_audio_segment(file_path: Path) -> AudioSegment:
66
- """
67
- Load an audio file as an AudioSegment. Convert to mp3 format in-memory if needed.
68
- """
69
- ext = file_path.suffix.lower()[1:]
70
- audio = AudioSegment.from_file(file_path.as_posix(), format=ext if ext != "mp3" else None)
71
- if ext != "mp3":
72
- buffer = BytesIO()
73
- audio.export(buffer, format="mp3")
74
- buffer.seek(0)
75
- audio = AudioSegment.from_file(buffer, format="mp3")
76
- return audio
77
-
78
-
79
- def split_audio(audio: AudioSegment, max_duration_s: int) -> list[AudioSegment]:
80
- """
81
- Split the AudioSegment into chunks no longer than max_duration_s seconds.
82
- """
83
- chunk_length_ms = (max_duration_s - 1) * 1000
84
- duration_ms = len(audio)
85
- segments: list[AudioSegment] = []
86
- segment_idx: int = 0
87
- for start_ms in range(0, duration_ms, chunk_length_ms):
88
- end_ms = min(start_ms + chunk_length_ms, duration_ms)
89
- segment = cast(AudioSegment, audio[start_ms:end_ms])
90
- segments.append(segment)
91
- # with open(f"segment_{segment_idx}.mp3", "wb") as f:
92
- # segment.export(f, format="mp3")
93
- segment_idx += 1
94
- return segments
95
-
96
-
97
- def transcribe_segment(segment: AudioSegment, client: OpenAI, model: str) -> str:
98
- """
99
- Transcribe a single AudioSegment chunk and return its text.
100
- """
101
- buffer = BytesIO()
102
- segment.export(buffer, format="mp3")
103
- buffer.seek(0)
104
- mp3_bytes = buffer.read()
105
- response = client.audio.transcriptions.create(
106
- model=model,
107
- prompt="Transcribe whole text from audio.",
108
- file=("audio.mp3", mp3_bytes),
109
- response_format="text",
110
- stream=True,
111
- )
112
- for res in response:
113
- if res.type == "transcript.text.delta":
114
- print(res.delta, end="", flush=True)
115
- if res.type == "transcript.text.done":
116
- print()
117
- return res.text
118
- else:
119
- raise RuntimeError("No transcription result found.")
120
-
121
-
122
- if __name__ == "__main__":
123
- TranscriptionApiArguments().run()
1
+ # pyright: reportUnknownVariableType=false, reportUnknownMemberType=false, reportArgumentType=false, reportMissingTypeStubs=false
2
+
3
+ from io import BytesIO
4
+ from pathlib import Path
5
+ from typing import Optional, cast
6
+
7
+ from openai import OpenAI
8
+ from pydub import AudioSegment
9
+ from spargear import BaseArguments
10
+
11
+ # Maximum chunk length in seconds
12
+ MAX_CHUNK_DURATION = 600
13
+
14
+
15
+ class TranscriptionApiArguments(BaseArguments):
16
+ input: Path
17
+ """The audio file to transcribe."""
18
+ output: Optional[Path] = None
19
+ """Path to save the transcription output."""
20
+ model: str = "gpt-4o-transcribe"
21
+ """The model to use for transcription."""
22
+ api_key: Optional[str] = None
23
+ """The API key for authentication."""
24
+ base_url: str = "https://api.openai.com/v1"
25
+ """The base URL for the API."""
26
+ prompt: str = "Transcribe whole text from audio."
27
+ """The prompt to use for transcription."""
28
+
29
+ def run(self) -> None:
30
+ model = self.model
31
+
32
+ client = OpenAI(api_key=self.api_key, base_url=self.base_url)
33
+
34
+ audio = load_audio_segment(self.input)
35
+
36
+ segments = split_audio(audio, MAX_CHUNK_DURATION)
37
+ print(f"[i] Audio duration: {len(audio) / 1000:.1f}s; splitting into {len(segments)} segment(s)")
38
+
39
+ transcripts: list[str] = []
40
+ for idx, seg in enumerate(segments, start=1):
41
+ print(f"[i] Transcribing segment {idx}/{len(segments)}...")
42
+ transcripts.append(transcribe_segment(seg, client, model, self.prompt))
43
+
44
+ full_transcript = "\n\n".join(transcripts)
45
+ output_path: Path = self.output or self.input.with_suffix(".txt")
46
+ output_path.write_text(full_transcript, encoding="utf-8")
47
+ print(f"[✓] Transcription saved to: {output_path}")
48
+
49
+
50
+ def load_audio_segment(file_path: Path) -> AudioSegment:
51
+ """
52
+ Load an audio file as an AudioSegment. Convert to mp3 format in-memory if needed.
53
+ """
54
+ ext = file_path.suffix.lower()[1:]
55
+ audio = AudioSegment.from_file(file_path.as_posix(), format=ext if ext != "mp3" else None)
56
+ if ext != "mp3":
57
+ buffer = BytesIO()
58
+ audio.export(buffer, format="mp3")
59
+ buffer.seek(0)
60
+ audio = AudioSegment.from_file(buffer, format="mp3")
61
+ return audio
62
+
63
+
64
+ def split_audio(audio: AudioSegment, max_duration_s: int) -> list[AudioSegment]:
65
+ """
66
+ Split the AudioSegment into chunks no longer than max_duration_s seconds.
67
+ """
68
+ chunk_length_ms = (max_duration_s - 1) * 1000
69
+ duration_ms = len(audio)
70
+ segments: list[AudioSegment] = []
71
+ segment_idx: int = 0
72
+ for start_ms in range(0, duration_ms, chunk_length_ms):
73
+ end_ms = min(start_ms + chunk_length_ms, duration_ms)
74
+ segment = cast(AudioSegment, audio[start_ms:end_ms])
75
+ segments.append(segment)
76
+ # with open(f"segment_{segment_idx}.mp3", "wb") as f:
77
+ # segment.export(f, format="mp3")
78
+ segment_idx += 1
79
+ return segments
80
+
81
+
82
+ def transcribe_segment(segment: AudioSegment, client: OpenAI, model: str, prompt: str) -> str:
83
+ """
84
+ Transcribe a single AudioSegment chunk and return its text.
85
+ """
86
+ buffer = BytesIO()
87
+ segment.export(buffer, format="mp3")
88
+ buffer.seek(0)
89
+ mp3_bytes = buffer.read()
90
+ response = client.audio.transcriptions.create(
91
+ model=model,
92
+ prompt=prompt,
93
+ file=("audio.mp3", mp3_bytes),
94
+ response_format="text",
95
+ stream=True,
96
+ )
97
+ for res in response:
98
+ if res.type == "transcript.text.delta":
99
+ print(res.delta, end="", flush=True)
100
+ if res.type == "transcript.text.done":
101
+ print()
102
+ return res.text
103
+ else:
104
+ raise RuntimeError("No transcription result found.")
105
+
106
+
107
+ def main() -> None:
108
+ TranscriptionApiArguments().run()
109
+
110
+
111
+ if __name__ == "__main__":
112
+ main()