session-slides 0.2.0 → 0.2.2

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.
package/README.md CHANGED
@@ -36,7 +36,6 @@ Output defaults to `./session-slides/{timestamp}.html` in your current directory
36
36
  | `--output PATH` | Output HTML file path (default: `./session-slides/{timestamp}.html`) |
37
37
  | `--title TEXT` | Custom presentation title |
38
38
  | `--open` | Open in browser after generation |
39
- | `--ai-titles` | Use Ollama for slide titles (requires local Ollama) |
40
39
  | `--clean` | Remove previous timestamped output files |
41
40
  | `--verbose` | Enable verbose output |
42
41
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "session-slides",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Convert Claude Code session transcripts into navigable HTML slide presentations",
5
5
  "keywords": [
6
6
  "claude",
@@ -33,6 +33,10 @@
33
33
  "linux",
34
34
  "win32"
35
35
  ],
36
+ "scripts": {
37
+ "test": "python3 -m pytest scripts/tests/ -q",
38
+ "prepublishOnly": "python3 -m pytest scripts/tests/ -q"
39
+ },
36
40
  "dependencies": {
37
41
  "cross-spawn": "^7.0.3"
38
42
  }
@@ -12,7 +12,6 @@ Options:
12
12
  --output PATH Output HTML file (default: ./session-slides/{timestamp}.html)
13
13
  --title TEXT Custom presentation title
14
14
  --open Open in browser after generation
15
- --ai-titles Use Ollama for title generation (requires ollama)
16
15
  --clean Remove previous output files before generating
17
16
  """
18
17
 
@@ -25,7 +24,7 @@ from typing import Optional
25
24
 
26
25
  # Import from sibling modules
27
26
  from parser import Session, Turn, extract_turns, find_current_session, load_session
28
- from titles import generate_turn_title, generate_continued_title, generate_title_ollama
27
+ from titles import generate_turn_title, generate_continued_title
29
28
  from truncation import (
30
29
  TruncationConfig,
31
30
  truncate_user_prompt,
@@ -51,22 +50,14 @@ def print_error(message: str) -> None:
51
50
  print(f"[✗] {message}", file=sys.stderr)
52
51
 
53
52
 
54
- def generate_titles_for_session(session: Session, use_ai: bool = False) -> dict[int, str]:
53
+ def generate_titles_for_session(session: Session) -> dict[int, str]:
55
54
  """Generate titles for all turns in a session."""
56
55
  titles = {}
57
56
 
58
57
  for turn in session.turns:
59
58
  if turn.is_user_message():
60
59
  prompt = turn.get_text_content()
61
- if use_ai:
62
- # Try AI first, fall back to heuristic
63
- ai_title = generate_title_ollama(prompt)
64
- if ai_title:
65
- titles[turn.number if hasattr(turn, 'number') else len(titles) + 1] = ai_title
66
- else:
67
- titles[turn.number if hasattr(turn, 'number') else len(titles) + 1] = generate_turn_title(prompt, len(titles) + 1)
68
- else:
69
- titles[turn.number if hasattr(turn, 'number') else len(titles) + 1] = generate_turn_title(prompt, len(titles) + 1)
60
+ titles[turn.number if hasattr(turn, 'number') else len(titles) + 1] = generate_turn_title(prompt, len(titles) + 1)
70
61
 
71
62
  return titles
72
63
 
@@ -159,7 +150,6 @@ def main() -> int:
159
150
  Examples:
160
151
  python generate_slides.py --from session.jsonl
161
152
  python generate_slides.py --from session.jsonl --output slides.html --title "My Session"
162
- python generate_slides.py --from session.jsonl --ai-titles --open
163
153
  python generate_slides.py # Auto-detect latest session file
164
154
  """,
165
155
  )
@@ -194,12 +184,6 @@ Examples:
194
184
  help="Open the generated HTML in default browser",
195
185
  )
196
186
 
197
- parser.add_argument(
198
- "--ai-titles",
199
- action="store_true",
200
- help="Use Ollama AI to generate slide titles (requires local Ollama)",
201
- )
202
-
203
187
  parser.add_argument(
204
188
  "--verbose",
205
189
  "-v",
@@ -249,10 +233,7 @@ Examples:
249
233
  print_success(f"Parsed {len(session.turns)} messages ({user_turns} user turns)")
250
234
 
251
235
  # Step 3: Convert to dict format and generate titles
252
- if args.ai_titles:
253
- print_progress("Generating AI titles with Ollama...")
254
- else:
255
- print_progress("Generating heuristic titles...")
236
+ print_progress("Generating slide titles...")
256
237
 
257
238
  session_dict = session_to_dict(session)
258
239
  print_success(f"Generated {session_dict['total_turns']} slide titles")
@@ -1056,6 +1056,8 @@ def _format_datetime(dt_string: str) -> tuple:
1056
1056
  """
1057
1057
  Parse and format a datetime string into readable date and time components.
1058
1058
 
1059
+ Converts UTC timestamps to the local timezone before formatting.
1060
+
1059
1061
  Args:
1060
1062
  dt_string: ISO format datetime string or similar
1061
1063
 
@@ -1065,32 +1067,33 @@ def _format_datetime(dt_string: str) -> tuple:
1065
1067
  if not dt_string:
1066
1068
  return None, None
1067
1069
 
1068
- # Handle timezone offset by stripping it (e.g., +00:00)
1069
- clean_dt = re.sub(r'[+-]\d{2}:\d{2}$', '', dt_string)
1070
-
1071
- # Try various datetime formats
1072
- formats = [
1073
- '%Y-%m-%dT%H:%M:%S.%fZ',
1074
- '%Y-%m-%dT%H:%M:%SZ',
1075
- '%Y-%m-%dT%H:%M:%S.%f',
1076
- '%Y-%m-%dT%H:%M:%S',
1077
- '%Y-%m-%d %H:%M:%S.%f',
1078
- '%Y-%m-%d %H:%M:%S',
1079
- '%Y-%m-%d',
1080
- ]
1081
-
1070
+ # Try Python's fromisoformat first (handles +00:00, Z, microseconds)
1082
1071
  dt = None
1083
- for fmt in formats:
1084
- try:
1085
- dt = datetime.strptime(clean_dt, fmt)
1086
- break
1087
- except ValueError:
1088
- continue
1072
+ try:
1073
+ dt = datetime.fromisoformat(dt_string.replace('Z', '+00:00'))
1074
+ except (ValueError, AttributeError):
1075
+ # Fall back to manual parsing for non-standard formats
1076
+ clean_dt = re.sub(r'[+-]\d{2}:\d{2}$', '', dt_string)
1077
+ for fmt in [
1078
+ '%Y-%m-%dT%H:%M:%S.%f',
1079
+ '%Y-%m-%dT%H:%M:%S',
1080
+ '%Y-%m-%d %H:%M:%S.%f',
1081
+ '%Y-%m-%d %H:%M:%S',
1082
+ '%Y-%m-%d',
1083
+ ]:
1084
+ try:
1085
+ dt = datetime.strptime(clean_dt, fmt)
1086
+ break
1087
+ except ValueError:
1088
+ continue
1089
1089
 
1090
1090
  if dt is None:
1091
- # Return the raw string if we can't parse it
1092
1091
  return dt_string, None
1093
1092
 
1093
+ # Convert to local timezone if the datetime is timezone-aware
1094
+ if dt.tzinfo is not None:
1095
+ dt = dt.astimezone()
1096
+
1094
1097
  # Format nicely: "February 4, 2026" and "2:30 PM"
1095
1098
  date_str = dt.strftime('%B %d, %Y').replace(' 0', ' ') # Remove leading zero from day
1096
1099
  time_str = dt.strftime('%I:%M %p').lstrip('0') # Remove leading zero from hour
@@ -1120,28 +1123,18 @@ def _calculate_duration(turns: list) -> str:
1120
1123
  if len(timestamps) < 2:
1121
1124
  return None
1122
1125
 
1123
- # Strip timezone offsets before parsing (e.g., +00:00)
1124
- first_ts = re.sub(r'[+-]\d{2}:\d{2}$', '', timestamps[0])
1125
- last_ts = re.sub(r'[+-]\d{2}:\d{2}$', '', timestamps[-1])
1126
-
1127
- # Try to parse first and last timestamps
1128
- formats = [
1129
- '%Y-%m-%dT%H:%M:%S.%fZ',
1130
- '%Y-%m-%dT%H:%M:%SZ',
1131
- '%Y-%m-%dT%H:%M:%S.%f',
1132
- '%Y-%m-%dT%H:%M:%S',
1133
- ]
1134
-
1126
+ # Parse first and last timestamps
1135
1127
  first_dt = None
1136
1128
  last_dt = None
1137
-
1138
- for fmt in formats:
1129
+ for ts_str, target in [(timestamps[0], 'first'), (timestamps[-1], 'last')]:
1139
1130
  try:
1140
- first_dt = datetime.strptime(first_ts, fmt)
1141
- last_dt = datetime.strptime(last_ts, fmt)
1142
- break
1143
- except ValueError:
1144
- continue
1131
+ dt = datetime.fromisoformat(ts_str.replace('Z', '+00:00'))
1132
+ except (ValueError, AttributeError):
1133
+ dt = None
1134
+ if target == 'first':
1135
+ first_dt = dt
1136
+ else:
1137
+ last_dt = dt
1145
1138
 
1146
1139
  if first_dt is None or last_dt is None:
1147
1140
  return None
package/scripts/titles.py CHANGED
@@ -944,84 +944,6 @@ def generate_continued_title(base_title: str) -> str:
944
944
  return f"{base_title} (continued)"
945
945
 
946
946
 
947
- def generate_title_ollama(
948
- prompt: str,
949
- turn_number: int,
950
- model: str = "llama3.2",
951
- host: str = "http://localhost:11434",
952
- timeout: float = 5.0
953
- ) -> str:
954
- """
955
- Generate a title using Ollama for AI-enhanced extraction.
956
-
957
- Falls back to pattern-based generation if Ollama is unavailable.
958
-
959
- Args:
960
- prompt: The user's prompt text
961
- turn_number: The turn number for fallback
962
- model: The Ollama model to use
963
- host: The Ollama API host
964
- timeout: Request timeout in seconds
965
-
966
- Returns:
967
- An AI-generated or pattern-based title
968
-
969
- Examples:
970
- >>> # With Ollama running:
971
- >>> generate_title_ollama("Can you help me create a REST API with authentication?", 1)
972
- 'Creating REST API Authentication'
973
-
974
- >>> # Without Ollama (falls back to pattern matching):
975
- >>> generate_title_ollama("fix the broken login", 2)
976
- 'Fixing Broken Login'
977
- """
978
- # Try Ollama first
979
- try:
980
- import requests
981
-
982
- system_prompt = """You are a title generator. Given a user's request, generate a short (2-5 word)
983
- descriptive title in gerund form (e.g., "Creating Login Form", "Fixing Database Bug").
984
-
985
- Rules:
986
- - Start with a gerund (verb ending in -ing)
987
- - Keep it concise and specific
988
- - Use title case
989
- - Do not include punctuation
990
- - Output ONLY the title, nothing else"""
991
-
992
- response = requests.post(
993
- f"{host}/api/generate",
994
- json={
995
- "model": model,
996
- "prompt": f"Generate a title for this request: {prompt}",
997
- "system": system_prompt,
998
- "stream": False,
999
- "options": {
1000
- "temperature": 0.3,
1001
- "num_predict": 20,
1002
- }
1003
- },
1004
- timeout=timeout
1005
- )
1006
-
1007
- if response.status_code == 200:
1008
- result = response.json()
1009
- title = result.get("response", "").strip()
1010
-
1011
- # Validate the title
1012
- if title and 2 <= len(title.split()) <= 6:
1013
- # Clean up any quotes or extra punctuation
1014
- title = title.strip("\"'.,;:!?")
1015
- return title
1016
-
1017
- except Exception:
1018
- # Ollama not available or error - fall through to pattern matching
1019
- pass
1020
-
1021
- # Fallback to pattern-based generation
1022
- return generate_turn_title(prompt, turn_number)
1023
-
1024
-
1025
947
  # Module-level convenience for testing
1026
948
  if __name__ == "__main__":
1027
949
  # Test examples