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/__init__.py +10 -1
- convoviz/analysis/__init__.py +16 -3
- convoviz/analysis/graphs.py +30 -6
- convoviz/analysis/wordcloud.py +68 -29
- convoviz/cli.py +51 -3
- convoviz/config.py +21 -4
- convoviz/interactive.py +123 -119
- convoviz/io/assets.py +12 -1
- convoviz/io/loaders.py +5 -0
- convoviz/io/writers.py +7 -3
- convoviz/logging_config.py +69 -0
- convoviz/models/conversation.py +18 -0
- convoviz/models/message.py +81 -5
- convoviz/pipeline.py +90 -73
- convoviz/renderers/markdown.py +96 -3
- convoviz/renderers/yaml.py +4 -0
- convoviz-0.4.7.dist-info/METADATA +233 -0
- {convoviz-0.2.12.dist-info → convoviz-0.4.7.dist-info}/RECORD +20 -19
- {convoviz-0.2.12.dist-info → convoviz-0.4.7.dist-info}/WHEEL +2 -2
- convoviz-0.2.12.dist-info/METADATA +0 -148
- {convoviz-0.2.12.dist-info → convoviz-0.4.7.dist-info}/entry_points.txt +0 -0
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.
|
|
9
|
-
from convoviz.
|
|
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
|
|
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
|
-
#
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
#
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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/
|
|
183
|
+
"➡️ https://github.com/mohamed-chs/convoviz 🔗\n\n"
|
|
167
184
|
)
|
convoviz/renderers/markdown.py
CHANGED
|
@@ -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 citeturnXsearchY.
|
|
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: turn0search18turn0search3
|
|
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
|
convoviz/renderers/yaml.py
CHANGED
|
@@ -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
|
+

|
|
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=
|
|
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=
|
|
4
|
-
convoviz/analysis/graphs.py,sha256=
|
|
5
|
-
convoviz/analysis/wordcloud.py,sha256=
|
|
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=
|
|
40
|
-
convoviz/config.py,sha256=
|
|
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=
|
|
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=
|
|
45
|
-
convoviz/io/loaders.py,sha256=
|
|
46
|
-
convoviz/io/writers.py,sha256
|
|
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=
|
|
50
|
-
convoviz/models/message.py,sha256=
|
|
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=
|
|
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=
|
|
56
|
-
convoviz/renderers/yaml.py,sha256=
|
|
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.
|
|
59
|
-
convoviz-0.
|
|
60
|
-
convoviz-0.
|
|
61
|
-
convoviz-0.
|
|
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,,
|