brickfrog commited on
Commit
f6427c1
·
verified ·
1 Parent(s): 4de67d6

Upload folder using huggingface_hub

Browse files
Files changed (4) hide show
  1. README.md +48 -5
  2. ankigen_core/cli.py +352 -0
  3. pyproject.toml +8 -0
  4. uv.lock +9 -1
README.md CHANGED
@@ -10,16 +10,18 @@ sdk_version: 5.38.1
10
 
11
  # AnkiGen - Anki Card Generator
12
 
13
- AnkiGen is a Gradio-based web application that generates Anki flashcards using OpenAI's GPT models. It creates CSV and `.apkg` deck files with subject-specific card generation.
14
 
15
  ## Features
16
 
17
  - Generate Anki cards for various subjects or from provided text/URLs
 
18
  - Create structured learning paths for complex topics
19
  - Export to CSV or `.apkg` format with default styling
20
  - Customizable number of topics and cards per topic
21
  - Built-in quality review system
22
- - User-friendly Gradio interface
 
23
 
24
  ## Installation
25
 
@@ -35,15 +37,54 @@ Preferred usage: [uv](https://github.com/astral-sh/uv)
35
 
36
  2. Install dependencies:
37
  ```bash
 
38
  uv pip install -e .
 
 
 
39
  ```
40
 
41
  3. Set up your OpenAI API key:
42
- - Create a `.env` file in the project root
43
- - Add: `OPENAI_API_KEY="your_api_key_here"`
 
44
 
45
  ## Usage
46
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  1. Run the application:
48
  ```bash
49
  uv run python app.py
@@ -63,10 +104,12 @@ Preferred usage: [uv](https://github.com/astral-sh/uv)
63
 
64
  ## Project Structure
65
 
66
- - `app.py`: Main Gradio application
67
  - `ankigen_core/`: Core logic modules
 
68
  - `agents/`: Agent system implementation
69
  - `card_generator.py`: Card generation orchestration
 
70
  - `learning_path.py`: Learning path analysis
71
  - `exporters.py`: CSV and `.apkg` export functionality
72
  - `models.py`: Data structures
 
10
 
11
  # AnkiGen - Anki Card Generator
12
 
13
+ AnkiGen generates Anki flashcards using OpenAI's GPT models. Available as both a web interface (Gradio) and command-line tool, it creates CSV and `.apkg` deck files with intelligent auto-configuration.
14
 
15
  ## Features
16
 
17
  - Generate Anki cards for various subjects or from provided text/URLs
18
+ - AI-powered auto-configuration (intelligently determines topics, card counts, and models)
19
  - Create structured learning paths for complex topics
20
  - Export to CSV or `.apkg` format with default styling
21
  - Customizable number of topics and cards per topic
22
  - Built-in quality review system
23
+ - **CLI for quick terminal-based generation**
24
+ - **Web interface for interactive use**
25
 
26
  ## Installation
27
 
 
37
 
38
  2. Install dependencies:
39
  ```bash
40
+ # Base installation (web interface)
41
  uv pip install -e .
42
+
43
+ # With CLI support
44
+ uv pip install -e ".[cli]"
45
  ```
46
 
47
  3. Set up your OpenAI API key:
48
+ ```bash
49
+ export OPENAI_API_KEY="your_api_key_here"
50
+ ```
51
 
52
  ## Usage
53
 
54
+ ### CLI (Quick & Direct)
55
+
56
+ Generate flashcards directly from your terminal with intelligent auto-configuration:
57
+
58
+ ```bash
59
+ # Quick generation (auto-detects best settings)
60
+ uv run python -m ankigen_core.cli -p "Basic SQL"
61
+
62
+ # Custom settings
63
+ uv run python -m ankigen_core.cli -p "React Hooks" \
64
+ --topics 5 \
65
+ --cards-per-topic 8 \
66
+ --output hooks.apkg
67
+
68
+ # Export to CSV
69
+ uv run python -m ankigen_core.cli -p "Docker basics" \
70
+ --format csv \
71
+ -o docker.csv
72
+
73
+ # Skip confirmation prompt
74
+ uv run python -m ankigen_core.cli -p "Python Lists" --no-confirm
75
+ ```
76
+
77
+ **CLI Options:**
78
+ - `-p, --prompt`: Subject/topic (required)
79
+ - `--topics`: Number of topics (auto-detected if omitted)
80
+ - `--cards-per-topic`: Cards per topic (auto-detected if omitted)
81
+ - `--model`: Model choice (`gpt-4.1` or `gpt-4.1-nano`)
82
+ - `-o, --output`: Output file path
83
+ - `--format`: Export format (`apkg` or `csv`)
84
+ - `--no-confirm`: Skip confirmation prompt
85
+
86
+ ### Web Interface (Interactive)
87
+
88
  1. Run the application:
89
  ```bash
90
  uv run python app.py
 
104
 
105
  ## Project Structure
106
 
107
+ - `app.py`: Main Gradio web application
108
  - `ankigen_core/`: Core logic modules
109
+ - `cli.py`: Command-line interface
110
  - `agents/`: Agent system implementation
111
  - `card_generator.py`: Card generation orchestration
112
+ - `auto_config.py`: AI-powered auto-configuration
113
  - `learning_path.py`: Learning path analysis
114
  - `exporters.py`: CSV and `.apkg` export functionality
115
  - `models.py`: Data structures
ankigen_core/cli.py ADDED
@@ -0,0 +1,352 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """CLI interface for AnkiGen - Generate Anki flashcards from the command line"""
2
+
3
+ import asyncio
4
+ import os
5
+ import sys
6
+ from pathlib import Path
7
+ from typing import Optional
8
+
9
+ import click
10
+ import pandas as pd
11
+ from rich.console import Console
12
+ from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn
13
+ from rich.table import Table
14
+ from rich.panel import Panel
15
+
16
+ from ankigen_core.auto_config import AutoConfigService
17
+ from ankigen_core.card_generator import orchestrate_card_generation
18
+ from ankigen_core.exporters import export_dataframe_to_apkg, export_dataframe_to_csv
19
+ from ankigen_core.llm_interface import OpenAIClientManager
20
+ from ankigen_core.utils import ResponseCache, get_logger
21
+
22
+ console = Console()
23
+ logger = get_logger()
24
+
25
+
26
+ def get_api_key() -> str:
27
+ """Get OpenAI API key from env or prompt user"""
28
+ api_key = os.getenv("OPENAI_API_KEY")
29
+
30
+ if not api_key:
31
+ console.print("[yellow]OpenAI API key not found in environment[/yellow]")
32
+ api_key = click.prompt("Enter your OpenAI API key", hide_input=True)
33
+
34
+ return api_key
35
+
36
+
37
+ async def auto_configure_from_prompt(
38
+ prompt: str,
39
+ api_key: str,
40
+ override_topics: Optional[int] = None,
41
+ override_cards: Optional[int] = None,
42
+ override_model: Optional[str] = None,
43
+ ) -> dict:
44
+ """Auto-configure settings from a prompt using AI analysis"""
45
+
46
+ with Progress(
47
+ SpinnerColumn(),
48
+ TextColumn("[progress.description]{task.description}"),
49
+ console=console,
50
+ ) as progress:
51
+ progress.add_task("Analyzing subject...", total=None)
52
+
53
+ # Initialize client
54
+ client_manager = OpenAIClientManager()
55
+ await client_manager.initialize_client(api_key)
56
+ openai_client = client_manager.get_client()
57
+
58
+ # Get auto-config
59
+ auto_config_service = AutoConfigService()
60
+ config = await auto_config_service.auto_configure(prompt, openai_client)
61
+
62
+ # Apply overrides
63
+ if override_topics is not None:
64
+ config["topic_number"] = override_topics
65
+ if override_cards is not None:
66
+ config["cards_per_topic"] = override_cards
67
+ if override_model is not None:
68
+ config["model_choice"] = override_model
69
+
70
+ # Display configuration
71
+ table = Table(
72
+ title="Auto-Configuration", show_header=True, header_style="bold cyan"
73
+ )
74
+ table.add_column("Setting", style="dim")
75
+ table.add_column("Value", style="green")
76
+
77
+ table.add_row("Topics", str(config.get("topic_number", "N/A")))
78
+ table.add_row("Cards per Topic", str(config.get("cards_per_topic", "N/A")))
79
+ table.add_row(
80
+ "Total Cards",
81
+ str(config.get("topic_number", 0) * config.get("cards_per_topic", 0)),
82
+ )
83
+ table.add_row("Model", config.get("model_choice", "N/A"))
84
+
85
+ if config.get("library_name"):
86
+ table.add_row("Library", config.get("library_name"))
87
+ if config.get("library_topic"):
88
+ table.add_row("Library Topic", config.get("library_topic"))
89
+ if config.get("preference_prompt"):
90
+ table.add_row(
91
+ "Learning Focus", config.get("preference_prompt", "")[:50] + "..."
92
+ )
93
+
94
+ console.print(table)
95
+
96
+ return config
97
+
98
+
99
+ async def generate_cards_from_config(
100
+ prompt: str,
101
+ config: dict,
102
+ api_key: str,
103
+ ) -> tuple:
104
+ """Generate cards using the configuration"""
105
+
106
+ client_manager = OpenAIClientManager()
107
+ response_cache = ResponseCache()
108
+
109
+ with Progress(
110
+ SpinnerColumn(),
111
+ TextColumn("[progress.description]{task.description}"),
112
+ BarColumn(),
113
+ TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
114
+ console=console,
115
+ ) as progress:
116
+ task = progress.add_task(
117
+ f"Generating {config['topic_number'] * config['cards_per_topic']} cards...",
118
+ total=100,
119
+ )
120
+
121
+ # Generate cards
122
+ (
123
+ output_df,
124
+ total_cards_html,
125
+ token_usage_html,
126
+ ) = await orchestrate_card_generation(
127
+ client_manager=client_manager,
128
+ cache=response_cache,
129
+ api_key_input=api_key,
130
+ subject=prompt,
131
+ generation_mode="subject",
132
+ source_text="",
133
+ url_input="",
134
+ model_name=config.get("model_choice", "gpt-4.1-nano"),
135
+ topic_number=config.get("topic_number", 3),
136
+ cards_per_topic=config.get("cards_per_topic", 5),
137
+ preference_prompt=config.get("preference_prompt", ""),
138
+ generate_cloze=config.get("generate_cloze_checkbox", False),
139
+ library_name=config.get("library_name")
140
+ if config.get("library_name")
141
+ else None,
142
+ library_topic=config.get("library_topic")
143
+ if config.get("library_topic")
144
+ else None,
145
+ )
146
+
147
+ progress.update(task, completed=100)
148
+
149
+ return output_df, total_cards_html, token_usage_html
150
+
151
+
152
+ def export_cards(
153
+ df: pd.DataFrame,
154
+ output_path: str,
155
+ deck_name: str,
156
+ export_format: str = "apkg",
157
+ ) -> str:
158
+ """Export cards to file"""
159
+
160
+ with Progress(
161
+ SpinnerColumn(),
162
+ TextColumn("[progress.description]{task.description}"),
163
+ console=console,
164
+ ) as progress:
165
+ progress.add_task(f"Exporting to {export_format.upper()}...", total=None)
166
+
167
+ if export_format == "apkg":
168
+ # Ensure .apkg extension
169
+ if not output_path.endswith(".apkg"):
170
+ output_path = (
171
+ output_path.replace(".csv", ".apkg")
172
+ if ".csv" in output_path
173
+ else f"{output_path}.apkg"
174
+ )
175
+
176
+ exported_path = export_dataframe_to_apkg(df, output_path, deck_name)
177
+ else: # csv
178
+ # Ensure .csv extension
179
+ if not output_path.endswith(".csv"):
180
+ output_path = (
181
+ output_path.replace(".apkg", ".csv")
182
+ if ".apkg" in output_path
183
+ else f"{output_path}.csv"
184
+ )
185
+
186
+ exported_path = export_dataframe_to_csv(df, output_path)
187
+
188
+ return exported_path
189
+
190
+
191
+ @click.command()
192
+ @click.option(
193
+ "-p",
194
+ "--prompt",
195
+ required=True,
196
+ help="Subject or topic for flashcard generation (e.g., 'Basic SQL', 'React Hooks')",
197
+ )
198
+ @click.option(
199
+ "--topics",
200
+ type=int,
201
+ help="Number of topics (auto-detected if not specified)",
202
+ )
203
+ @click.option(
204
+ "--cards-per-topic",
205
+ type=int,
206
+ help="Number of cards per topic (auto-detected if not specified)",
207
+ )
208
+ @click.option(
209
+ "--model",
210
+ type=click.Choice(["gpt-4.1", "gpt-4.1-nano"], case_sensitive=False),
211
+ help="Model to use for generation (auto-selected if not specified)",
212
+ )
213
+ @click.option(
214
+ "-o",
215
+ "--output",
216
+ default="deck.apkg",
217
+ help="Output file path (default: deck.apkg)",
218
+ )
219
+ @click.option(
220
+ "--format",
221
+ "export_format",
222
+ type=click.Choice(["apkg", "csv"], case_sensitive=False),
223
+ default="apkg",
224
+ help="Export format (default: apkg)",
225
+ )
226
+ @click.option(
227
+ "--api-key",
228
+ envvar="OPENAI_API_KEY",
229
+ help="OpenAI API key (or set OPENAI_API_KEY env var)",
230
+ )
231
+ @click.option(
232
+ "--no-confirm",
233
+ is_flag=True,
234
+ help="Skip confirmation prompt",
235
+ )
236
+ def main(
237
+ prompt: str,
238
+ topics: Optional[int],
239
+ cards_per_topic: Optional[int],
240
+ model: Optional[str],
241
+ output: str,
242
+ export_format: str,
243
+ api_key: Optional[str],
244
+ no_confirm: bool,
245
+ ):
246
+ """
247
+ AnkiGen CLI - Generate Anki flashcards from the command line
248
+
249
+ Examples:
250
+
251
+ # Quick generation with auto-config
252
+ ankigen -p "Basic SQL"
253
+
254
+ # With custom settings
255
+ ankigen -p "React Hooks" --topics 5 --cards-per-topic 8 --output hooks.apkg
256
+
257
+ # Export to CSV
258
+ ankigen -p "Docker basics" --format csv -o docker.csv
259
+ """
260
+
261
+ # Print header
262
+ console.print(
263
+ Panel.fit(
264
+ "[bold cyan]AnkiGen CLI[/bold cyan]\n[dim]Generate Anki flashcards with AI[/dim]",
265
+ border_style="cyan",
266
+ )
267
+ )
268
+ console.print()
269
+
270
+ # Get API key
271
+ if not api_key:
272
+ api_key = get_api_key()
273
+
274
+ # Run async workflow
275
+ async def workflow():
276
+ try:
277
+ # Step 1: Auto-configure
278
+ console.print(f"[bold]Subject:[/bold] {prompt}\n")
279
+ config = await auto_configure_from_prompt(
280
+ prompt=prompt,
281
+ api_key=api_key,
282
+ override_topics=topics,
283
+ override_cards=cards_per_topic,
284
+ override_model=model,
285
+ )
286
+
287
+ # Step 2: Confirm (unless --no-confirm)
288
+ if not no_confirm:
289
+ console.print()
290
+ if not click.confirm("Proceed with card generation?", default=True):
291
+ console.print("[yellow]Cancelled[/yellow]")
292
+ return
293
+
294
+ console.print()
295
+
296
+ # Step 3: Generate cards
297
+ df, total_html, token_html = await generate_cards_from_config(
298
+ prompt=prompt,
299
+ config=config,
300
+ api_key=api_key,
301
+ )
302
+
303
+ if df.empty:
304
+ console.print("[red]✗[/red] No cards generated")
305
+ sys.exit(1)
306
+
307
+ # Step 4: Export
308
+ console.print()
309
+ deck_name = f"AnkiGen - {prompt}"
310
+ exported_path = export_cards(
311
+ df=df,
312
+ output_path=output,
313
+ deck_name=deck_name,
314
+ export_format=export_format,
315
+ )
316
+
317
+ # Step 5: Success summary
318
+ console.print()
319
+ file_size = Path(exported_path).stat().st_size / 1024 # KB
320
+
321
+ summary = Table.grid(padding=(0, 2))
322
+ summary.add_row("[green]✓[/green] Success!", "")
323
+ summary.add_row("Cards Generated:", f"[bold]{len(df)}[/bold]")
324
+ summary.add_row("Output File:", f"[bold]{exported_path}[/bold]")
325
+ summary.add_row("File Size:", f"{file_size:.1f} KB")
326
+
327
+ # Extract token count from HTML if available
328
+ if "tokens" in token_html.lower():
329
+ import re
330
+
331
+ token_match = re.search(r"(\d+)\s*tokens", token_html)
332
+ if token_match:
333
+ summary.add_row("Tokens Used:", token_match.group(1))
334
+
335
+ console.print(
336
+ Panel(summary, border_style="green", title="Generation Complete")
337
+ )
338
+
339
+ except KeyboardInterrupt:
340
+ console.print("\n[yellow]Cancelled by user[/yellow]")
341
+ sys.exit(130)
342
+ except Exception as e:
343
+ logger.error(f"CLI error: {e}", exc_info=True)
344
+ console.print(f"[red]✗ Error:[/red] {str(e)}")
345
+ sys.exit(1)
346
+
347
+ # Run the async workflow
348
+ asyncio.run(workflow())
349
+
350
+
351
+ if __name__ == "__main__":
352
+ main()
pyproject.toml CHANGED
@@ -35,6 +35,14 @@ dev = [
35
  "pre-commit>=4.3.0",
36
  "pytest-anyio>=0.0.0",
37
  ]
 
 
 
 
 
 
 
 
38
 
39
  [tool.setuptools]
40
  py-modules = ["app"]
 
35
  "pre-commit>=4.3.0",
36
  "pytest-anyio>=0.0.0",
37
  ]
38
+ cli = [
39
+ "click>=8.1.0",
40
+ "rich>=13.0.0",
41
+ "python-dotenv>=1.0.0",
42
+ ]
43
+
44
+ [project.scripts]
45
+ ankigen = "ankigen_core.cli:main"
46
 
47
  [tool.setuptools]
48
  py-modules = ["app"]
uv.lock CHANGED
@@ -34,6 +34,11 @@ dependencies = [
34
  ]
35
 
36
  [package.optional-dependencies]
 
 
 
 
 
37
  dev = [
38
  { name = "black" },
39
  { name = "pre-commit" },
@@ -48,6 +53,7 @@ dev = [
48
  requires-dist = [
49
  { name = "beautifulsoup4", specifier = "==4.13.5" },
50
  { name = "black", marker = "extra == 'dev'", specifier = ">=25.9.0" },
 
51
  { name = "fastmcp", specifier = ">=2.12.3" },
52
  { name = "genanki", specifier = ">=0.13.1" },
53
  { name = "gradio", specifier = ">=5.47.0" },
@@ -61,11 +67,13 @@ requires-dist = [
61
  { name = "pytest-anyio", marker = "extra == 'dev'", specifier = ">=0.0.0" },
62
  { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=7.0.0" },
63
  { name = "pytest-mock", marker = "extra == 'dev'", specifier = ">=3.15.1" },
 
 
64
  { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.13.1" },
65
  { name = "tenacity", specifier = ">=9.1.2" },
66
  { name = "tiktoken", specifier = ">=0.11.0" },
67
  ]
68
- provides-extras = ["dev"]
69
 
70
  [[package]]
71
  name = "annotated-types"
 
34
  ]
35
 
36
  [package.optional-dependencies]
37
+ cli = [
38
+ { name = "click" },
39
+ { name = "python-dotenv" },
40
+ { name = "rich" },
41
+ ]
42
  dev = [
43
  { name = "black" },
44
  { name = "pre-commit" },
 
53
  requires-dist = [
54
  { name = "beautifulsoup4", specifier = "==4.13.5" },
55
  { name = "black", marker = "extra == 'dev'", specifier = ">=25.9.0" },
56
+ { name = "click", marker = "extra == 'cli'", specifier = ">=8.1.0" },
57
  { name = "fastmcp", specifier = ">=2.12.3" },
58
  { name = "genanki", specifier = ">=0.13.1" },
59
  { name = "gradio", specifier = ">=5.47.0" },
 
67
  { name = "pytest-anyio", marker = "extra == 'dev'", specifier = ">=0.0.0" },
68
  { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=7.0.0" },
69
  { name = "pytest-mock", marker = "extra == 'dev'", specifier = ">=3.15.1" },
70
+ { name = "python-dotenv", marker = "extra == 'cli'", specifier = ">=1.0.0" },
71
+ { name = "rich", marker = "extra == 'cli'", specifier = ">=13.0.0" },
72
  { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.13.1" },
73
  { name = "tenacity", specifier = ">=9.1.2" },
74
  { name = "tiktoken", specifier = ">=0.11.0" },
75
  ]
76
+ provides-extras = ["dev", "cli"]
77
 
78
  [[package]]
79
  name = "annotated-types"