Fss-Rag-Mini/asciinema_to_gif.py
BobAi 4166d0a362 Initial release: FSS-Mini-RAG - Lightweight semantic code search system
🎯 Complete transformation from 5.9GB bloated system to 70MB optimized solution

 Key Features:
- Hybrid embedding system (Ollama + ML fallback + hash backup)
- Intelligent chunking with language-aware parsing
- Semantic + BM25 hybrid search with rich context
- Zero-config portable design with graceful degradation
- Beautiful TUI for beginners + powerful CLI for experts
- Comprehensive documentation with 8+ Mermaid diagrams
- Professional animated demo (183KB optimized GIF)

🏗️ Architecture Highlights:
- LanceDB vector storage with streaming indexing
- Smart file tracking (size/mtime) to avoid expensive rehashing
- Progressive chunking: Markdown headers → Python functions → fixed-size
- Quality filtering: 200+ chars, 20+ words, 30% alphanumeric content
- Concurrent batch processing with error recovery

📦 Package Contents:
- Core engine: claude_rag/ (11 modules, 2,847 lines)
- Entry points: rag-mini (unified), rag-tui (beginner interface)
- Documentation: README + 6 guides with visual diagrams
- Assets: 3D icon, optimized demo GIF, recording tools
- Tests: 8 comprehensive integration and validation tests
- Examples: Usage patterns, config templates, dependency analysis

🎥 Demo System:
- Scripted demonstration showing 12 files → 58 chunks indexing
- Semantic search with multi-line result previews
- Complete workflow from TUI startup to CLI mastery
- Professional recording pipeline with asciinema + GIF conversion

🛡️ Security & Quality:
- Complete .gitignore with personal data protection
- Dependency optimization (removed python-dotenv)
- Code quality validation and educational test suite
- Agent-reviewed architecture and documentation

Ready for production use - copy folder, run ./rag-mini, start searching\!
2025-08-12 16:38:28 +10:00

290 lines
9.9 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Asciinema to GIF Converter
Converts .cast files to optimized GIF animations without external services.
"""
import os
import sys
import json
import argparse
import subprocess
from pathlib import Path
from typing import List, Dict, Any
import tempfile
import shutil
class AsciinemaToGIF:
def __init__(self):
self.temp_dir = None
def check_dependencies(self) -> Dict[str, bool]:
"""Check if required tools are available."""
tools = {
'ffmpeg': self._check_command('ffmpeg'),
'convert': self._check_command('convert'), # ImageMagick
'gifsicle': self._check_command('gifsicle') # Optional optimizer
}
return tools
def _check_command(self, command: str) -> bool:
"""Check if a command is available."""
return shutil.which(command) is not None
def install_instructions(self):
"""Show installation instructions for missing dependencies."""
print("📦 Required Dependencies:")
print()
print("Ubuntu/Debian:")
print(" sudo apt install ffmpeg imagemagick gifsicle")
print()
print("macOS:")
print(" brew install ffmpeg imagemagick gifsicle")
print()
print("Arch Linux:")
print(" sudo pacman -S ffmpeg imagemagick gifsicle")
def parse_cast_file(self, cast_path: Path) -> Dict[str, Any]:
"""Parse asciinema .cast file."""
with open(cast_path, 'r') as f:
lines = f.readlines()
# First line is header
header = json.loads(lines[0])
# Remaining lines are events
events = []
for line in lines[1:]:
if line.strip():
events.append(json.loads(line))
return {
'header': header,
'events': events,
'width': header.get('width', 80),
'height': header.get('height', 24)
}
def create_frames(self, cast_data: Dict[str, Any], output_dir: Path) -> List[Path]:
"""Create individual frame images from cast data."""
print("🎬 Creating frames...")
width = cast_data['width']
height = cast_data['height']
events = cast_data['events']
# Terminal state
screen = [[' ' for _ in range(width)] for _ in range(height)]
cursor_x, cursor_y = 0, 0
frames = []
frame_count = 0
last_time = 0
for event in events:
timestamp, event_type, data = event
# Calculate delay
delay = timestamp - last_time
last_time = timestamp
if event_type == 'o': # Output event
# Process terminal output
for char in data:
if char == '\n':
cursor_y += 1
cursor_x = 0
if cursor_y >= height:
# Scroll up
screen = screen[1:] + [[' ' for _ in range(width)]]
cursor_y = height - 1
elif char == '\r':
cursor_x = 0
elif char == '\033':
# Skip ANSI escape sequences (simplified)
continue
elif char.isprintable():
if cursor_x < width and cursor_y < height:
screen[cursor_y][cursor_x] = char
cursor_x += 1
# Create frame if significant delay or content change
if delay > 0.1 or frame_count == 0:
frame_path = self._create_frame_image(screen, output_dir, frame_count, delay)
frames.append((frame_path, delay))
frame_count += 1
return frames
def _create_frame_image(self, screen: List[List[str]], output_dir: Path,
frame_num: int, delay: float) -> Path:
"""Create a single frame image using ImageMagick."""
# Convert screen to text
text_content = []
for row in screen:
line = ''.join(row).rstrip()
text_content.append(line)
# Create text file
text_file = output_dir / f"frame_{frame_num:04d}.txt"
with open(text_file, 'w') as f:
f.write('\n'.join(text_content))
# Convert to image using ImageMagick
image_file = output_dir / f"frame_{frame_num:04d}.png"
cmd = [
'convert',
'-font', 'Liberation-Mono', # Monospace font
'-pointsize', '12',
'-background', '#1e1e1e', # Dark background
'-fill', '#d4d4d4', # Light text
'-gravity', 'NorthWest',
f'label:@{text_file}',
str(image_file)
]
try:
subprocess.run(cmd, check=True, capture_output=True)
return image_file
except subprocess.CalledProcessError as e:
print(f"❌ Failed to create frame {frame_num}: {e}")
return None
def create_gif(self, frames: List[tuple], output_path: Path, fps: int = 10) -> bool:
"""Create GIF from frame images using ffmpeg."""
print("🎞️ Creating GIF...")
if not frames:
print("❌ No frames to process")
return False
# Create ffmpeg input file list
input_list = self.temp_dir / "input_list.txt"
with open(input_list, 'w') as f:
for frame_path, delay in frames:
if frame_path and frame_path.exists():
duration = max(delay, 0.1) # Minimum 0.1s per frame
f.write(f"file '{frame_path}'\n")
f.write(f"duration {duration}\n")
# Create GIF with ffmpeg
cmd = [
'ffmpeg',
'-f', 'concat',
'-safe', '0',
'-i', str(input_list),
'-vf', 'fps=10,scale=800:-1:flags=lanczos,palettegen=reserve_transparent=0',
'-y',
str(output_path)
]
try:
subprocess.run(cmd, check=True, capture_output=True)
return True
except subprocess.CalledProcessError as e:
print(f"❌ FFmpeg failed: {e}")
return False
def optimize_gif(self, gif_path: Path) -> bool:
"""Optimize GIF using gifsicle."""
if not self._check_command('gifsicle'):
return True # Skip if not available
print("🗜️ Optimizing GIF...")
optimized_path = gif_path.with_suffix('.optimized.gif')
cmd = [
'gifsicle',
'-O3',
'--lossy=80',
'--colors', '256',
str(gif_path),
'-o', str(optimized_path)
]
try:
subprocess.run(cmd, check=True, capture_output=True)
# Replace original with optimized
shutil.move(optimized_path, gif_path)
return True
except subprocess.CalledProcessError as e:
print(f"⚠️ Optimization failed: {e}")
return False
def convert(self, cast_path: Path, output_path: Path, fps: int = 10) -> bool:
"""Convert asciinema cast file to GIF."""
print(f"🎯 Converting {cast_path.name} to GIF...")
# Check dependencies
deps = self.check_dependencies()
missing = [tool for tool, available in deps.items() if not available and tool != 'gifsicle']
if missing:
print(f"❌ Missing required tools: {', '.join(missing)}")
print()
self.install_instructions()
return False
# Create temporary directory
self.temp_dir = Path(tempfile.mkdtemp(prefix='asciinema_gif_'))
try:
# Parse cast file
print("📖 Parsing cast file...")
cast_data = self.parse_cast_file(cast_path)
# Create frames
frames = self.create_frames(cast_data, self.temp_dir)
if not frames:
print("❌ No frames created")
return False
# Create GIF
success = self.create_gif(frames, output_path, fps)
if success:
# Optimize
self.optimize_gif(output_path)
# Show results
size_mb = output_path.stat().st_size / (1024 * 1024)
print(f"✅ GIF created: {output_path}")
print(f"📏 Size: {size_mb:.2f} MB")
return True
else:
return False
finally:
# Cleanup
if self.temp_dir and self.temp_dir.exists():
shutil.rmtree(self.temp_dir)
def main():
parser = argparse.ArgumentParser(description='Convert asciinema recordings to GIF')
parser.add_argument('input', type=Path, help='Input .cast file')
parser.add_argument('-o', '--output', type=Path, help='Output .gif file (default: same name as input)')
parser.add_argument('--fps', type=int, default=10, help='Frames per second (default: 10)')
args = parser.parse_args()
if not args.input.exists():
print(f"❌ Input file not found: {args.input}")
sys.exit(1)
if not args.output:
args.output = args.input.with_suffix('.gif')
converter = AsciinemaToGIF()
success = converter.convert(args.input, args.output, args.fps)
if success:
print("🎉 Conversion complete!")
else:
print("💥 Conversion failed!")
sys.exit(1)
if __name__ == '__main__':
main()