convoviz 0.2.12__py3-none-any.whl → 0.4.7__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.
convoviz/pipeline.py CHANGED
@@ -1,22 +1,22 @@
1
1
  """Main processing pipeline for convoviz."""
2
2
 
3
+ import logging
3
4
  from pathlib import Path
4
5
  from shutil import rmtree
5
6
 
6
7
  from rich.console import Console
7
8
 
8
- from convoviz.analysis.graphs import generate_graphs
9
- from convoviz.analysis.wordcloud import generate_wordclouds
10
- from convoviz.config import ConvovizConfig
11
- from convoviz.exceptions import InvalidZipError
9
+ from convoviz.config import ConvovizConfig, OutputKind
10
+ from convoviz.exceptions import ConfigurationError, InvalidZipError
12
11
  from convoviz.io.loaders import (
13
12
  find_latest_bookmarklet_json,
14
13
  load_collection_from_json,
15
14
  load_collection_from_zip,
16
15
  )
17
- from convoviz.io.writers import save_collection, save_custom_instructions
16
+ from convoviz.io.writers import save_collection
18
17
 
19
18
  console = Console()
19
+ logger = logging.getLogger(__name__)
20
20
 
21
21
 
22
22
  def _safe_uri(path: Path) -> str:
@@ -48,6 +48,7 @@ def run_pipeline(config: ConvovizConfig) -> None:
48
48
  if not input_path.exists():
49
49
  raise InvalidZipError(str(input_path), reason="File does not exist")
50
50
 
51
+ logger.info(f"Starting pipeline with input: {input_path}")
51
52
  console.print(f"Loading data from {input_path} [bold yellow]📂[/bold yellow] ...\n")
52
53
 
53
54
  # Load collection based on input type
@@ -64,6 +65,7 @@ def run_pipeline(config: ConvovizConfig) -> None:
64
65
  else:
65
66
  # Assume zip
66
67
  collection = load_collection_from_zip(input_path)
68
+ logger.info(f"Loaded collection with {len(collection.conversations)} conversations")
67
69
 
68
70
  # Try to merge bookmarklet data if available
69
71
  bookmarklet_json = find_latest_bookmarklet_json()
@@ -72,6 +74,7 @@ def run_pipeline(config: ConvovizConfig) -> None:
72
74
  try:
73
75
  bookmarklet_collection = load_collection_from_json(bookmarklet_json)
74
76
  collection.update(bookmarklet_collection)
77
+ logger.info("Merged bookmarklet data")
75
78
  except Exception as e:
76
79
  console.print(
77
80
  f"[bold yellow]Warning:[/bold yellow] Failed to load bookmarklet data: {e}"
@@ -80,10 +83,21 @@ def run_pipeline(config: ConvovizConfig) -> None:
80
83
  output_folder = config.output_folder
81
84
  output_folder.mkdir(parents=True, exist_ok=True)
82
85
 
83
- # Clean only specific sub-directories we manage
84
- managed_dirs = ["Markdown", "Graphs", "Word-Clouds"]
85
- for d in managed_dirs:
86
- sub_dir = output_folder / d
86
+ # Determine which outputs are selected
87
+ selected_outputs = config.outputs
88
+
89
+ # Build mapping of output kind -> directory name
90
+ output_dir_map: dict[OutputKind, str] = {
91
+ OutputKind.MARKDOWN: "Markdown",
92
+ OutputKind.GRAPHS: "Graphs",
93
+ OutputKind.WORDCLOUDS: "Word-Clouds",
94
+ }
95
+
96
+ # Clean only specific sub-directories we manage (only for selected outputs)
97
+ for output_kind, dir_name in output_dir_map.items():
98
+ if output_kind not in selected_outputs:
99
+ continue
100
+ sub_dir = output_folder / dir_name
87
101
  if sub_dir.exists():
88
102
  # Never follow symlinks; just unlink them.
89
103
  if sub_dir.is_symlink():
@@ -94,74 +108,77 @@ def run_pipeline(config: ConvovizConfig) -> None:
94
108
  sub_dir.unlink()
95
109
  sub_dir.mkdir(exist_ok=True)
96
110
 
97
- # Clean specific files we manage
98
- managed_files = ["custom_instructions.json"]
99
- for f in managed_files:
100
- managed_file = output_folder / f
101
- if managed_file.exists():
102
- if managed_file.is_symlink() or managed_file.is_file():
103
- managed_file.unlink()
104
- elif managed_file.is_dir():
105
- rmtree(managed_file)
106
- else:
107
- managed_file.unlink()
108
-
109
- # Save markdown files
110
- markdown_folder = output_folder / "Markdown"
111
- save_collection(
112
- collection,
113
- markdown_folder,
114
- config.conversation,
115
- config.message.author_headers,
116
- folder_organization=config.folder_organization,
117
- progress_bar=True,
118
- )
119
- console.print(
120
- f"\nDone [bold green]✅[/bold green] ! "
121
- f"Check the output [bold blue]📄[/bold blue] here: {_safe_uri(markdown_folder)} 🔗\n"
122
- )
123
-
124
- # Generate graphs
125
- graph_folder = output_folder / "Graphs"
126
- graph_folder.mkdir(parents=True, exist_ok=True)
127
- generate_graphs(
128
- collection,
129
- graph_folder,
130
- config.graph,
131
- progress_bar=True,
132
- )
133
- console.print(
134
- f"\nDone [bold green]✅[/bold green] ! "
135
- f"Check the output [bold blue]📈[/bold blue] here: {_safe_uri(graph_folder)} 🔗\n"
136
- )
137
-
138
- # Generate word clouds
139
- wordcloud_folder = output_folder / "Word-Clouds"
140
- wordcloud_folder.mkdir(parents=True, exist_ok=True)
141
- generate_wordclouds(
142
- collection,
143
- wordcloud_folder,
144
- config.wordcloud,
145
- progress_bar=True,
146
- )
147
- console.print(
148
- f"\nDone [bold green]✅[/bold green] ! "
149
- f"Check the output [bold blue]🔡☁️[/bold blue] here: {_safe_uri(wordcloud_folder)} 🔗\n"
150
- )
151
-
152
- # Save custom instructions
153
- console.print("Writing custom instructions [bold blue]📝[/bold blue] ...\n")
154
- instructions_path = output_folder / "custom_instructions.json"
155
- save_custom_instructions(collection, instructions_path)
156
- console.print(
157
- f"\nDone [bold green]✅[/bold green] ! "
158
- f"Check the output [bold blue]📝[/bold blue] here: {_safe_uri(instructions_path)} 🔗\n"
159
- )
111
+ # Save markdown files (if selected)
112
+ if OutputKind.MARKDOWN in selected_outputs:
113
+ markdown_folder = output_folder / "Markdown"
114
+ save_collection(
115
+ collection,
116
+ markdown_folder,
117
+ config.conversation,
118
+ config.message.author_headers,
119
+ folder_organization=config.folder_organization,
120
+ progress_bar=True,
121
+ )
122
+ logger.info("Markdown generation complete")
123
+ console.print(
124
+ f"\nDone [bold green]✅[/bold green] ! "
125
+ f"Check the output [bold blue]📄[/bold blue] here: {_safe_uri(markdown_folder)} 🔗\n"
126
+ )
127
+
128
+ # Generate graphs (if selected)
129
+ if OutputKind.GRAPHS in selected_outputs:
130
+ # Lazy import to allow markdown-only usage without matplotlib
131
+ try:
132
+ from convoviz.analysis.graphs import generate_graphs
133
+ except ModuleNotFoundError as e:
134
+ raise ConfigurationError(
135
+ "Graph generation requires matplotlib. "
136
+ 'Reinstall with the [viz] extra: uv tool install "convoviz[viz]"'
137
+ ) from e
138
+
139
+ graph_folder = output_folder / "Graphs"
140
+ graph_folder.mkdir(parents=True, exist_ok=True)
141
+ generate_graphs(
142
+ collection,
143
+ graph_folder,
144
+ config.graph,
145
+ progress_bar=True,
146
+ )
147
+ logger.info("Graph generation complete")
148
+ console.print(
149
+ f"\nDone [bold green][/bold green] ! "
150
+ f"Check the output [bold blue]📈[/bold blue] here: {_safe_uri(graph_folder)} 🔗\n"
151
+ )
152
+
153
+ # Generate word clouds (if selected)
154
+ if OutputKind.WORDCLOUDS in selected_outputs:
155
+ # Lazy import to allow markdown-only usage without wordcloud/nltk
156
+ try:
157
+ from convoviz.analysis.wordcloud import generate_wordclouds
158
+ except ModuleNotFoundError as e:
159
+ raise ConfigurationError(
160
+ "Word cloud generation requires wordcloud and nltk. "
161
+ 'Reinstall with the [viz] extra: uv tool install "convoviz[viz]"'
162
+ ) from e
163
+
164
+ wordcloud_folder = output_folder / "Word-Clouds"
165
+ wordcloud_folder.mkdir(parents=True, exist_ok=True)
166
+ generate_wordclouds(
167
+ collection,
168
+ wordcloud_folder,
169
+ config.wordcloud,
170
+ progress_bar=True,
171
+ )
172
+ logger.info("Wordcloud generation complete")
173
+ console.print(
174
+ f"\nDone [bold green]✅[/bold green] ! "
175
+ f"Check the output [bold blue]🔡☁️[/bold blue] here: {_safe_uri(wordcloud_folder)} 🔗\n"
176
+ )
160
177
 
161
178
  console.print(
162
179
  "ALL DONE [bold green]🎉🎉🎉[/bold green] !\n\n"
163
180
  f"Explore the full gallery [bold yellow]🖼️[/bold yellow] at: {_safe_uri(output_folder)} 🔗\n\n"
164
181
  "I hope you enjoy the outcome 🤞.\n\n"
165
182
  "If you appreciate it, kindly give the project a star 🌟 on GitHub:\n\n"
166
- "➡️ https://github.com/mohamed-chs/chatgpt-history-export-to-md 🔗\n\n"
183
+ "➡️ https://github.com/mohamed-chs/convoviz 🔗\n\n"
167
184
  )
@@ -2,6 +2,7 @@
2
2
 
3
3
  import re
4
4
  from collections.abc import Callable
5
+ from typing import Any
5
6
 
6
7
  from convoviz.config import AuthorHeaders, ConversationConfig
7
8
  from convoviz.exceptions import MessageContentError
@@ -9,6 +10,82 @@ from convoviz.models import Conversation, Node
9
10
  from convoviz.renderers.yaml import render_yaml_header
10
11
 
11
12
 
13
+ def replace_citations(
14
+ text: str,
15
+ citations: list[dict[str, Any]] | None = None,
16
+ citation_map: dict[str, dict[str, str | None]] | None = None,
17
+ ) -> str:
18
+ """Replace citation placeholders in text with markdown links.
19
+
20
+ Supports two formats:
21
+ 1. Tether v4 (metadata.citations): Placed at specific indices (【...】 placeholders).
22
+ 2. Embedded (Tether v3?): Unicode markers citeturnXsearchY.
23
+
24
+ Args:
25
+ text: The original message text
26
+ citations: List of tether v4 citation objects (start_ix/end_ix)
27
+ citation_map: Map of internal citation IDs to metadata (turnXsearchY -> {title, url})
28
+
29
+ Returns:
30
+ Text with all placeholders replaced by markdown links
31
+ """
32
+ # 1. Handle Tether v4 (Index-based replacements)
33
+ if citations:
34
+ # Sort citations by start_ix descending to replace safely from end
35
+ sorted_citations = sorted(citations, key=lambda c: c.get("start_ix", 0), reverse=True)
36
+
37
+ for cit in sorted_citations:
38
+ start = cit.get("start_ix")
39
+ end = cit.get("end_ix")
40
+ meta = cit.get("metadata", {})
41
+
42
+ if start is None or end is None:
43
+ continue
44
+
45
+ replacement = _format_link(meta.get("title"), meta.get("url"))
46
+
47
+ # Only replace if strictly positive indices and bounds check
48
+ if 0 <= start < end <= len(text):
49
+ text = text[:start] + replacement + text[end:]
50
+
51
+ # 2. Handle Embedded Citations (Regex-based)
52
+ # Pattern: cite (key)+ 
53
+ # Codepoints: \uE200 (Start), \uE202 (Sep), \uE201 (End)
54
+ if citation_map is not None:
55
+ pattern = re.compile(r"\uE200cite((?:\uE202[a-zA-Z0-9]+)+)\uE201")
56
+
57
+ def replacer(match: re.Match) -> str:
58
+ # Group 1 contains string like: turn0search18turn0search3
59
+ # Split by separator \uE202 (first item will be empty string)
60
+ raw_keys = match.group(1).split("\ue202")
61
+ keys = [k for k in raw_keys if k]
62
+
63
+ links = []
64
+ for key in keys:
65
+ if key in citation_map:
66
+ data = citation_map[key]
67
+ link = _format_link(data.get("title"), data.get("url"))
68
+ if link:
69
+ links.append(link)
70
+
71
+ return "".join(links)
72
+
73
+ text = pattern.sub(replacer, text)
74
+
75
+ return text
76
+
77
+
78
+ def _format_link(title: str | None, url: str | None) -> str:
79
+ """Format a title and URL into a concise markdown link."""
80
+ if title and url:
81
+ return f" [[{title}]({url})]"
82
+ elif url:
83
+ return f" [[Source]({url})]"
84
+ elif title:
85
+ return f" [{title}]"
86
+ return ""
87
+
88
+
12
89
  def close_code_blocks(text: str) -> str:
13
90
  """Ensure all code blocks in the text are properly closed.
14
91
 
@@ -137,6 +214,7 @@ def render_node(
137
214
  use_dollar_latex: bool = False,
138
215
  asset_resolver: Callable[[str], str | None] | None = None,
139
216
  flavor: str = "standard",
217
+ citation_map: dict[str, dict[str, str | None]] | None = None,
140
218
  ) -> str:
141
219
  """Render a complete node as markdown.
142
220
 
@@ -146,9 +224,7 @@ def render_node(
146
224
  use_dollar_latex: Whether to convert LaTeX delimiters to dollars
147
225
  asset_resolver: Function to resolve asset IDs to paths
148
226
  flavor: Markdown flavor ("standard" or "obsidian")
149
-
150
- Returns:
151
- Complete markdown string for the node
227
+ citation_map: Global map of citations
152
228
  """
153
229
  if node.message is None:
154
230
  return ""
@@ -185,6 +261,19 @@ def render_node(
185
261
  # Some message types only contain non-text parts; those still may have images.
186
262
  text = ""
187
263
 
264
+ # Process citations if present (Tether v4 metadata or Embedded v3)
265
+ # Use global citation_map if provided, merging/falling back to local if needed.
266
+ # Actually, local internal map is subset of global map if we aggregated correctly.
267
+ # So we prefer the passed global map.
268
+ effective_map = citation_map or node.message.internal_citation_map
269
+
270
+ if node.message.metadata.citations or effective_map:
271
+ text = replace_citations(
272
+ text,
273
+ citations=node.message.metadata.citations,
274
+ citation_map=effective_map,
275
+ )
276
+
188
277
  content = close_code_blocks(text)
189
278
  content = f"\n{content}\n" if content else ""
190
279
  if use_dollar_latex:
@@ -255,6 +344,9 @@ def render_conversation(
255
344
  # Start with YAML header
256
345
  markdown = render_yaml_header(conversation, config.yaml)
257
346
 
347
+ # Pre-calculate citation map for the conversation
348
+ citation_map = conversation.citation_map
349
+
258
350
  # Render message nodes in a deterministic traversal order.
259
351
  for node in _ordered_nodes(conversation):
260
352
  if node.message:
@@ -264,6 +356,7 @@ def render_conversation(
264
356
  use_dollar_latex,
265
357
  asset_resolver=asset_resolver,
266
358
  flavor=flavor,
359
+ citation_map=citation_map,
267
360
  )
268
361
 
269
362
  return markdown
@@ -111,6 +111,10 @@ def render_yaml_header(conversation: Conversation, config: YAMLConfig) -> str:
111
111
  yaml_fields["content_types"] = conversation.content_types
112
112
  if config.custom_instructions:
113
113
  yaml_fields["custom_instructions"] = conversation.custom_instructions
114
+ if config.is_starred:
115
+ yaml_fields["is_starred"] = conversation.is_starred
116
+ if config.voice:
117
+ yaml_fields["voice"] = conversation.voice
114
118
 
115
119
  if not yaml_fields:
116
120
  return ""
@@ -0,0 +1,233 @@
1
+ Metadata-Version: 2.4
2
+ Name: convoviz
3
+ Version: 0.4.7
4
+ Summary: Convert your ChatGPT export (ZIP) into clean Markdown text files with inline media, and generate data visualizations like word clouds and usage graphs.
5
+ Keywords: markdown,chatgpt,openai,visualization,analytics,json,export,data-analysis,obsidian
6
+ Author: Mohamed Cheikh Sidiya
7
+ Author-email: Mohamed Cheikh Sidiya <mohamedcheikhsidiya77@gmail.com>
8
+ License-Expression: MIT
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Requires-Dist: orjson>=3.11.5
13
+ Requires-Dist: pydantic>=2.12.5
14
+ Requires-Dist: pydantic-settings>=2.7.0
15
+ Requires-Dist: questionary>=2.1.1
16
+ Requires-Dist: rich>=14.2.0
17
+ Requires-Dist: tqdm>=4.67.1
18
+ Requires-Dist: typer>=0.21.0
19
+ Requires-Dist: nltk>=3.9.2 ; extra == 'viz'
20
+ Requires-Dist: wordcloud>=1.9.5 ; extra == 'viz'
21
+ Requires-Python: >=3.12
22
+ Project-URL: Repository, https://github.com/mohamed-chs/convoviz
23
+ Provides-Extra: viz
24
+ Description-Content-Type: text/markdown
25
+
26
+ <p align="center">
27
+ <h1 align="center">Convoviz 📊</h1>
28
+ <p align="center"><strong>Visualize your entire ChatGPT data</strong></p>
29
+ <p align="center">
30
+ Convert your ChatGPT history into clean, readable Markdown (text files).
31
+ </p>
32
+ <p align="center"><strong>
33
+ Perfect for archiving, local search, or use with note-taking apps like Obsidian.
34
+ </strong></p>
35
+ <p align="center">
36
+ Visualize your data with word clouds 🔡☁️ and usage graphs 📈.
37
+ </p>
38
+ </p>
39
+
40
+ <p align="center">
41
+ <a href="https://pypi.org/project/convoviz/"><img src="https://img.shields.io/pypi/v/convoviz?style=for-the-badge&logo=python&logoColor=white" alt="PyPI Version"></a>
42
+ <a href="https://github.com/mohamed-chs/convoviz/blob/main/LICENSE"><img src="https://img.shields.io/pypi/l/convoviz?style=for-the-badge" alt="License"></a>
43
+ <a href="https://pepy.tech/projects/convoviz"><img src="https://img.shields.io/pepy/dt/convoviz?style=for-the-badge&color=blue" alt="Downloads"></a>
44
+ <a href="https://github.com/mohamed-chs/convoviz/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/mohamed-chs/convoviz/ci.yml?style=for-the-badge&logo=github&label=CI" alt="CI Status"></a>
45
+ </p>
46
+
47
+ ---
48
+
49
+ ## ✨ Features
50
+
51
+ | Feature | Description |
52
+ |---------|-------------|
53
+ | 📝 **Markdown Export** | Clean, well-formatted Markdown with optional YAML headers |
54
+ | 🖼️ **Inline Images** | Media attachments rendered directly in your Markdown files |
55
+ | ☁️ **Word Clouds** | Visual breakdowns of your most-used words and phrases |
56
+ | 📈 **Usage Graphs** | Bar plots and charts showing your conversation patterns |
57
+
58
+ > 💡 **See examples in the [`demo/`](https://github.com/mohamed-chs/convoviz/tree/main/demo) folder!**
59
+
60
+ ---
61
+
62
+ ## 📖 How to Use
63
+
64
+ ### Step 1: Export Your ChatGPT Data
65
+
66
+ 1. Sign in at [chatgpt.com](https://chatgpt.com)
67
+ 2. Navigate to: **Profile Name** (bottom left) → **Settings** → **Data controls** → **Export**
68
+ 3. Click **Confirm export**
69
+ 4. Wait for the email from OpenAI, then download the `.zip` file
70
+
71
+ ---
72
+
73
+ ### Step 2: Install Convoviz
74
+
75
+ ### 🚀 Quick Install
76
+
77
+ Run one of the commands below to install **everything** you need automatically.
78
+
79
+ #### 🍎 macOS / 🐧 Linux
80
+ 1. Open `Terminal`.
81
+ - **macOS**: Press `Cmd + Space`, type "Terminal", and hit Enter.
82
+ - **Linux**: Press `Ctrl + Alt + T`, or search "Terminal" in your app menu.
83
+ 2. Copy and paste this command:
84
+
85
+ ```bash
86
+ curl -fsSL https://raw.githubusercontent.com/mohamed-chs/convoviz/main/install.sh | bash
87
+ ```
88
+
89
+ #### 🪟 Windows
90
+ 1. Open `PowerShell`.
91
+ - Press the `Windows` key, type "PowerShell", and hit Enter.
92
+ 2. Copy and paste this command:
93
+
94
+ ```powershell
95
+ irm https://raw.githubusercontent.com/mohamed-chs/convoviz/main/install.ps1 | iex
96
+ ```
97
+
98
+ <details>
99
+ <summary><strong>📦 Alternative: Install with pip</strong></summary>
100
+
101
+ If you prefer using `pip` directly:
102
+
103
+ ```bash
104
+ # Create a virtual environment (keeps your system Python clean)
105
+ python3 -m venv .venv
106
+
107
+ # Activate the virtual environment
108
+ source .venv/bin/activate # On Windows: .venv\Scripts\activate
109
+
110
+ # Install convoviz with visualization extras
111
+ pip install "convoviz[viz]"
112
+ ```
113
+
114
+ </details>
115
+
116
+ ---
117
+
118
+ ### Step 3: Run Convoviz
119
+
120
+ The simplest way is to run this in your terminal and follow the interactive prompts:
121
+
122
+ ```bash
123
+ convoviz
124
+ ```
125
+
126
+ Or, provide arguments directly to skip the prompts:
127
+
128
+ ```bash
129
+ convoviz --input path/to/your/export.zip --output path/to/output/folder
130
+ ```
131
+
132
+ <details>
133
+ <summary><strong>⚙️ Command Line Options</strong></summary>
134
+
135
+ #### Selective Output
136
+
137
+ By default, all outputs (Markdown, graphs, word clouds) are generated. Use `--outputs` to pick specific ones:
138
+
139
+ ```bash
140
+ convoviz --input export.zip --outputs markdown --outputs graphs
141
+ ```
142
+
143
+ Available options: `markdown`, `graphs`, `wordclouds`
144
+
145
+ > In interactive mode, you'll be prompted to choose which outputs to generate.
146
+
147
+ #### Other Useful Flags
148
+
149
+ | Flag | Description |
150
+ |------|-------------|
151
+ | `--zip` / `-z` | Alias for `--input` (for convenience) |
152
+ | `--no-interactive` | Force non-interactive mode |
153
+ | `--flat` | Put all Markdown files in a single folder (instead of organizing by date) |
154
+ | `--verbose` / `-v` | Enable detailed logging (use `-vv` for debug logs) |
155
+ | `--log-file PATH` | Specify a custom log file location |
156
+
157
+ For a complete list of options:
158
+
159
+ ```bash
160
+ convoviz --help
161
+ ```
162
+
163
+ </details>
164
+
165
+ ---
166
+
167
+ ### Step 4: Check the Output 🎉
168
+
169
+ After running the script, head to your output folder (defaults to `Documents/ChatGPT-Data` if you didn't change it) to see:
170
+ - 📝 Neatly formatted Markdown files
171
+ - 📊 Visualizations and graphs
172
+
173
+ If you've had a great experience, consider giving the project a ⭐ **star**! It keeps me motivated and helps others discover it!
174
+
175
+ ![wordcloud example](https://raw.githubusercontent.com/mohamed-chs/convoviz/main/demo/wordcloud-example.png)
176
+
177
+ ---
178
+
179
+ ## 💌 Share Your Feedback!
180
+
181
+ I hope you find this tool useful. I'm continuously looking to improve on this, but I need your help for that.
182
+
183
+ Whether you're a tech wizard or you're new to all this, I'd love to hear about your journey with the tool. Found a quirk? Have a suggestion? Or just want to send some good vibes? I'm all ears!
184
+
185
+ 👉 **[Open an Issue](https://github.com/mohamed-chs/convoviz/issues)**
186
+
187
+ ---
188
+
189
+ ## 🤝 Contributing
190
+
191
+ Interested in contributing? Check out the **[Contributing Guide](https://github.com/mohamed-chs/convoviz/tree/main/CONTRIBUTING.md)** for development setup, code style, and how to submit a pull request.
192
+
193
+ ---
194
+
195
+ ## 📝 Notes
196
+
197
+ <details>
198
+ <summary><strong>Offline</strong></summary>
199
+
200
+ Word clouds use NLTK stopwords. If you're offline and NLTK data isn't installed yet, pre-download it:
201
+
202
+ ```bash
203
+ python -c "import nltk; nltk.download('stopwords')"
204
+ ```
205
+
206
+ **NOTE:** The install script already handles this, so you can immediately go offline after running it.
207
+
208
+ </details>
209
+
210
+ <details>
211
+ <summary><strong>About This Project</strong></summary>
212
+
213
+ This is just a small thing I coded to help me see my convos in beautiful markdown. It was originally built with [Obsidian](https://obsidian.md/) (my go-to note-taking app) in mind, but the default output is standard Markdown.
214
+
215
+ I wasn't a fan of the clunky, and sometimes paid, browser extensions.
216
+
217
+ It was also a great opportunity to learn more about Python and type annotations. I had mypy, pyright, and ruff all on strict mode, 'twas fun.
218
+
219
+ </details>
220
+
221
+ <details>
222
+ <summary><strong>Using as a Library</strong></summary>
223
+
224
+ It should also work as a library, so you can import and use the models and functions. I need to add more documentation for that though. Feel free to reach out if you need help.
225
+
226
+ </details>
227
+
228
+ <details>
229
+ <summary><strong>Bookmarklet (Experimental)</strong></summary>
230
+
231
+ There's also a JavaScript bookmarklet flow under `js/` for exporting additional conversation data outside the official ZIP export. This is experimental.
232
+
233
+ </details>
@@ -1,8 +1,8 @@
1
- convoviz/__init__.py,sha256=bQLCHO2U9EyMTGqNgsYiCtBQKTKNj4iIM3-TwIkrnRY,612
1
+ convoviz/__init__.py,sha256=UjwkFEmRXhE-3qhsoGTG9XhtoL2cP0o4h3Sp9fcA2Ic,858
2
2
  convoviz/__main__.py,sha256=1qiGW13_SgL7wJi8iioIN-AAHGkNGnEl5q_RcPUrI0s,143
3
- convoviz/analysis/__init__.py,sha256=FxgH5JJpyypiLJpMQn_HlM51jnb8lQdP63_C_W3Dlx4,241
4
- convoviz/analysis/graphs.py,sha256=gt056UkgGcy9vCkupQmW_HjOLy-W6j4Ekxr315BXPgA,29457
5
- convoviz/analysis/wordcloud.py,sha256=WLZhBo5Ha83o6XJyzjhgx4tDjgPX-Q9kizCxfmxlJ3A,4828
3
+ convoviz/analysis/__init__.py,sha256=1dHjnw88mepSY3Q3U8lEvSqkuWPtkzpyYMgq0CIWoXk,654
4
+ convoviz/analysis/graphs.py,sha256=OGLkyKx9Fq7r4jK-A46oy1NWttQ5F2MLxO4mfW6wkfc,29796
5
+ convoviz/analysis/wordcloud.py,sha256=3MfBg_Aq7-nz0Zc-FhkxEvTljImIQU0rmXZv81WUBDM,6178
6
6
  convoviz/assets/colormaps.txt,sha256=59TSGz428AxY14AEvymAH2IJ2RT9Mlp7Sy0N12NEdXQ,108
7
7
  convoviz/assets/fonts/AmaticSC-Regular.ttf,sha256=83clh7a3urnTLud0_yZofuIb6BdyC2LMI9jhE6G2LvU,142696
8
8
  convoviz/assets/fonts/ArchitectsDaughter-Regular.ttf,sha256=fnrj5_N_SlY2Lj3Ehqz5aKECPZVJlJAflgsOU94_qIM,37756
@@ -36,26 +36,27 @@ convoviz/assets/fonts/YsabeauSC-Regular.ttf,sha256=G4lkq34KKqZOaoomtxFz_KlwVmxg5
36
36
  convoviz/assets/fonts/YsabeauSC-Thin.ttf,sha256=hZGOZNTRrxbiUPE2VDeLbtnaRwkMOBaVQbq7Fwx-34c,116932
37
37
  convoviz/assets/fonts/Zeyada-Regular.ttf,sha256=fKhkrp9VHt_3Aw8JfkfkPeC2j3CilLWuPUudzBeawPQ,57468
38
38
  convoviz/assets/stopwords.txt,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
39
- convoviz/cli.py,sha256=TPboT0maH_b_EjiT9cWbUSyMFz4ozoqf1_R-4AzY31g,3730
40
- convoviz/config.py,sha256=LNVVK6ccTjS4Ip0JczoPIo2fFP1GdqCJOivtzOyZBqs,3107
39
+ convoviz/cli.py,sha256=nniH7QPbbH_buQJGa35vd3IEl7RvZsWRiLpUuhlxXaI,5314
40
+ convoviz/config.py,sha256=4L0gSOYUPWPEif6lJM1VhkkJq7rAZwAkMi5DIv1Pkwc,3677
41
41
  convoviz/exceptions.py,sha256=bQpIKls48uOQpagEJAxpXf5LF7QoagRRfbD0MjWC7Ak,1476
42
- convoviz/interactive.py,sha256=CwZOfu8a3xat4wGwD7MdVrvCd0rvvbtuufj2yWcGOzg,7424
42
+ convoviz/interactive.py,sha256=z4Xdhk_47R1Zx_CaPpY_Gq88i6l9A8YKN3mlc5Uz6KU,8284
43
43
  convoviz/io/__init__.py,sha256=y70TYypJ36_kaEA04E2wa1EDaKQVjprKItoKR6MMs4M,471
44
- convoviz/io/assets.py,sha256=WLauNEvk9QRo0Q52KE_bPyCRFa1CjM54L1j8SsTfGwg,2894
45
- convoviz/io/loaders.py,sha256=RuGiGzpyNcgwTxOM-m2ehhyh2mP1-k1YamK8-VynR3g,5713
46
- convoviz/io/writers.py,sha256=krGvU6UJMrDxZCqt3u_lyLA_gS0oSbeUXhYNFbY4mHo,6862
44
+ convoviz/io/assets.py,sha256=5zcZPlQa9niDw9o-sqJIKgLc0OJ9auzd6KAve5WfBkQ,3459
45
+ convoviz/io/loaders.py,sha256=SqmBWUBqT5lsCf01yy-FUhwIxbiKTFMQnj4k213DsGI,5891
46
+ convoviz/io/writers.py,sha256=-HTvj7D9sqM8M-RsGwd44AquxCVmcDVHgta22QlfNzU,7068
47
+ convoviz/logging_config.py,sha256=PRuOKij8UD6sKdg3lAsu9lUsTUZ3O6_6uffnyg07M1U,2060
47
48
  convoviz/models/__init__.py,sha256=6gAfrk6KJT2QxdvX_v15mUdfIqEw1xKxwQlKSfyA5eI,532
48
49
  convoviz/models/collection.py,sha256=L658yKMNC6IZrfxYxZBe-oO9COP_bzVfRznnNon7tfU,4467
49
- convoviz/models/conversation.py,sha256=ssx1Z6LM9kJIx3OucQW8JVoAc8zCdxj1iOLtie2B3ak,5678
50
- convoviz/models/message.py,sha256=0CJ9hJ1rQiesn1SgHqFgEgKUgS7XAPGtSunQl5q8Pl4,8316
50
+ convoviz/models/conversation.py,sha256=IZvDMXxbHSW3Hvxljm8ZpB5eJceJkJ3prDUvZOtrKyM,6419
51
+ convoviz/models/message.py,sha256=lJV51fVLaiIamcTG96VyVq5Khluyp6E_87BWynbxUXg,11591
51
52
  convoviz/models/node.py,sha256=1vBAtKVscYsUBDnKAOyLxuZaK9KoVF1dFXiKXRHxUnY,1946
52
- convoviz/pipeline.py,sha256=IKfyy3iaNDTqox2YvwB3tnPqvL5iM0i_kMoa854glvY,5806
53
+ convoviz/pipeline.py,sha256=1kLtsNDN3LYNudyPBlyKwQZ8zWCmRKveP3VWfIgichw,6765
53
54
  convoviz/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
54
55
  convoviz/renderers/__init__.py,sha256=IQgwD9NqtUgbS9zwyPBNZbBIZcFrbZ9C7WMAV-X3Xdg,261
55
- convoviz/renderers/markdown.py,sha256=55PACkd-F0mmBXWXQ5SrfJr3UNrK_z2spQnePdk1UsQ,7849
56
- convoviz/renderers/yaml.py,sha256=XG1s4VhDdx-TiqekTkgED87RZ1lVQ7IwrbA-sZHrs7k,4056
56
+ convoviz/renderers/markdown.py,sha256=uv6SshqY6Nuj374I8qpRXQSlCJ7pLF0IUBl0y-Nd3so,11323
57
+ convoviz/renderers/yaml.py,sha256=R6hjXCpgeVm3rPuPVgaj2VopfpPqRFxAFWY7Nxtf6Vg,4213
57
58
  convoviz/utils.py,sha256=IQEKYHhWOnYxlr4GwAHoquG0BXTlVRkORL80oUSaIeQ,3417
58
- convoviz-0.2.12.dist-info/WHEEL,sha256=eycQt0QpYmJMLKpE3X9iDk8R04v2ZF0x82ogq-zP6bQ,79
59
- convoviz-0.2.12.dist-info/entry_points.txt,sha256=HYsmsw5vt36yYHB05uVU48AK2WLkcwshly7m7KKuZMY,54
60
- convoviz-0.2.12.dist-info/METADATA,sha256=mB2Gy-w67xCSKmDxvMQM67MS695-T6cx3b_yFoKoQ-k,5639
61
- convoviz-0.2.12.dist-info/RECORD,,
59
+ convoviz-0.4.7.dist-info/WHEEL,sha256=fAguSjoiATBe7TNBkJwOjyL1Tt4wwiaQGtNtjRPNMQA,80
60
+ convoviz-0.4.7.dist-info/entry_points.txt,sha256=HYsmsw5vt36yYHB05uVU48AK2WLkcwshly7m7KKuZMY,54
61
+ convoviz-0.4.7.dist-info/METADATA,sha256=VbjYg-utjxShJNRT4p3Pf1Lp37TyJcE4FZbUzDE1j3s,7883
62
+ convoviz-0.4.7.dist-info/RECORD,,