storia 0.4.0__tar.gz
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.
- storia-0.4.0/PKG-INFO +10 -0
- storia-0.4.0/README.md +0 -0
- storia-0.4.0/pyproject.toml +27 -0
- storia-0.4.0/src/storia/__init__.py +12 -0
- storia-0.4.0/src/storia/ai.py +79 -0
- storia-0.4.0/src/storia/api_key_manager.py +62 -0
- storia-0.4.0/src/storia/constant.py +24 -0
- storia-0.4.0/src/storia/helper.py +38 -0
- storia-0.4.0/src/storia/languages/__init__.py +0 -0
- storia-0.4.0/src/storia/languages/italian.json +1000 -0
- storia-0.4.0/src/storia/main.py +166 -0
- storia-0.4.0/src/storia/prompt.py +75 -0
- storia-0.4.0/src/storia/py.typed +0 -0
storia-0.4.0/PKG-INFO
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: storia
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: A simple cli tool to help languages by using power of ai stories
|
|
5
|
+
Author: Pratham Das
|
|
6
|
+
Author-email: Pratham Das <prathamdas003@gmail.com>
|
|
7
|
+
Requires-Dist: groq>=1.1.2
|
|
8
|
+
Requires-Python: >=3.13
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
storia-0.4.0/README.md
ADDED
|
File without changes
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "storia"
|
|
3
|
+
version = "0.4.0"
|
|
4
|
+
description = "A simple cli tool to help languages by using power of ai stories"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Pratham Das", email = "prathamdas003@gmail.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.13"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"groq>=1.1.2",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[build-system]
|
|
15
|
+
requires = ["uv_build>=0.11.3,<0.12.0"]
|
|
16
|
+
build-backend = "uv_build"
|
|
17
|
+
|
|
18
|
+
[project.scripts]
|
|
19
|
+
storia= "storia.main:main"
|
|
20
|
+
|
|
21
|
+
[tool.setuptools.package-data]
|
|
22
|
+
"storia.laguages" = ["*.json"]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
[pypi]
|
|
26
|
+
username= "__token__"
|
|
27
|
+
password="pypi-AgEIcHlwaS5vcmcCJGE3YjFkMjMwLTdlZWEtNGFjMy1iMTFhLTg4OWM2MTQ1MWI1ZQACD1sxLFsic3F1aWxpbyJdXQACLFsyLFsiNmY0NDZmYjItNWU0Yy00MjFhLWFjMGItZDE3NzcxNmJmNzg1Il1dAAAGICLsuNIGwUoYeZXv_qkHyaxSEFb9BQQu-jxpDAlHG0lj"
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from .constant import Mode, DefaultConfig
|
|
2
|
+
from .main import main
|
|
3
|
+
from .helper import config_to_dict, write_config, get_random_topics
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"main",
|
|
7
|
+
"Mode",
|
|
8
|
+
"DefaultConfig",
|
|
9
|
+
"config_to_dict",
|
|
10
|
+
"write_config",
|
|
11
|
+
"get_random_topics",
|
|
12
|
+
]
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from groq import AsyncGroq
|
|
2
|
+
from .constant import Stories
|
|
3
|
+
from .api_key_manager import get_api_key_from_config
|
|
4
|
+
from .prompt import prompt_text
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_client() -> AsyncGroq:
|
|
9
|
+
"""Get Groq client with API key from config."""
|
|
10
|
+
api_key = get_api_key_from_config()
|
|
11
|
+
return AsyncGroq(api_key=api_key)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
async def generate_story(topics: list[tuple[str, str]]) -> str:
|
|
15
|
+
client = get_client()
|
|
16
|
+
models = ["openai/gpt-oss-20b", "llama-3.3-70b-versatile"]
|
|
17
|
+
|
|
18
|
+
last_error = None
|
|
19
|
+
for model in models:
|
|
20
|
+
try:
|
|
21
|
+
words = [word for word, _ in topics]
|
|
22
|
+
response = await client.chat.completions.create(
|
|
23
|
+
model=model,
|
|
24
|
+
messages=[
|
|
25
|
+
{"role": "system", "content": prompt_text},
|
|
26
|
+
{
|
|
27
|
+
"role": "user",
|
|
28
|
+
"content": f"Target language: Italian\nWord list: {words}",
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
temperature=1,
|
|
32
|
+
max_tokens=4000,
|
|
33
|
+
top_p=0.95,
|
|
34
|
+
)
|
|
35
|
+
story = response.choices[0].message.content
|
|
36
|
+
if story:
|
|
37
|
+
return story
|
|
38
|
+
except Exception as e:
|
|
39
|
+
last_error = str(e)
|
|
40
|
+
print(f"Model {model} failed: {e}", file=sys.stderr)
|
|
41
|
+
continue
|
|
42
|
+
|
|
43
|
+
print("Error: All AI models failed to generate a story.", file=sys.stderr)
|
|
44
|
+
print(f"Last error: {last_error}", file=sys.stderr)
|
|
45
|
+
print("Please check your API key and try again.", file=sys.stderr)
|
|
46
|
+
sys.exit(1)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def write_story(story: str, date: str) -> None:
|
|
50
|
+
parts = story.split("\n\n", 1)
|
|
51
|
+
|
|
52
|
+
if len(parts) != 2:
|
|
53
|
+
print("Error: AI returned an unexpected response format.", file=sys.stderr)
|
|
54
|
+
print(
|
|
55
|
+
"The story must have two parts separated by a blank line.", file=sys.stderr
|
|
56
|
+
)
|
|
57
|
+
print("Please try again.", file=sys.stderr)
|
|
58
|
+
sys.exit(1)
|
|
59
|
+
|
|
60
|
+
target_language_story = parts[0]
|
|
61
|
+
english_retelling = parts[1]
|
|
62
|
+
output = (
|
|
63
|
+
f"--------\n"
|
|
64
|
+
f"[{date}]\n\n"
|
|
65
|
+
f"{target_language_story}\n\n"
|
|
66
|
+
f"{english_retelling}\n"
|
|
67
|
+
f"--------\n\n"
|
|
68
|
+
)
|
|
69
|
+
print(output)
|
|
70
|
+
try:
|
|
71
|
+
with Stories.open("a", encoding="utf-8") as f:
|
|
72
|
+
f.write(output)
|
|
73
|
+
except FileNotFoundError:
|
|
74
|
+
print("Error: Could not save story to file.", file=sys.stderr)
|
|
75
|
+
print(f"Check that the directory exists: {Stories.parent}", file=sys.stderr)
|
|
76
|
+
sys.exit(1)
|
|
77
|
+
except Exception as e:
|
|
78
|
+
print(f"Error: Failed to save story: {e}", file=sys.stderr)
|
|
79
|
+
sys.exit(1)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from .constant import config
|
|
2
|
+
import json
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def get_api_key_from_config() -> str:
|
|
7
|
+
"""Read API key from config, raise if missing."""
|
|
8
|
+
if not config.exists():
|
|
9
|
+
print("Error: Config file not found.", file=sys.stderr)
|
|
10
|
+
print("Please run 'storia' to initialize your configuration.", file=sys.stderr)
|
|
11
|
+
sys.exit(1)
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
data = json.loads(config.read_text())
|
|
15
|
+
except json.JSONDecodeError:
|
|
16
|
+
print("Error: Config file is corrupted.", file=sys.stderr)
|
|
17
|
+
print("Try running 'storia --reset' to fix this.", file=sys.stderr)
|
|
18
|
+
sys.exit(1)
|
|
19
|
+
|
|
20
|
+
key = data.get("api_key")
|
|
21
|
+
if not key:
|
|
22
|
+
print("Error: API key not configured.", file=sys.stderr)
|
|
23
|
+
print("Please run 'storia' to add your Groq API key.", file=sys.stderr)
|
|
24
|
+
sys.exit(1)
|
|
25
|
+
return key
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def prompt_for_api_key() -> str:
|
|
29
|
+
"""Prompt user for API key with validation."""
|
|
30
|
+
while True:
|
|
31
|
+
key = input("Enter your Groq API key: ").strip()
|
|
32
|
+
if not key:
|
|
33
|
+
print("API key cannot be empty. Please try again.")
|
|
34
|
+
continue
|
|
35
|
+
if len(key) < 10:
|
|
36
|
+
print("API key seems too short. Please check and try again.")
|
|
37
|
+
continue
|
|
38
|
+
return key
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_or_prompt_api_key() -> str:
|
|
42
|
+
"""Get existing API key from config or prompt user if missing."""
|
|
43
|
+
if config.exists():
|
|
44
|
+
try:
|
|
45
|
+
data = json.loads(config.read_text())
|
|
46
|
+
if data.get("api_key"):
|
|
47
|
+
return data["api_key"]
|
|
48
|
+
except (json.JSONDecodeError, FileNotFoundError):
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
print("API key not found. Please enter your Groq API key.")
|
|
52
|
+
api_key = prompt_for_api_key()
|
|
53
|
+
|
|
54
|
+
if config.exists():
|
|
55
|
+
try:
|
|
56
|
+
data = json.loads(config.read_text())
|
|
57
|
+
data["api_key"] = api_key
|
|
58
|
+
config.write_text(json.dumps(data, indent=2))
|
|
59
|
+
except (json.JSONDecodeError, FileNotFoundError):
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
return api_key
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from importlib.resources import files
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from enum import Enum
|
|
5
|
+
|
|
6
|
+
HOME = Path.home()
|
|
7
|
+
DATA_DIR = HOME / ".storia"
|
|
8
|
+
config = DATA_DIR / "config.json"
|
|
9
|
+
Stories = DATA_DIR / "stories.txt"
|
|
10
|
+
content_path = files("storia.languages").joinpath("italian.json")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Mode(Enum):
|
|
14
|
+
RANDOM = 0
|
|
15
|
+
TOPIC_BASED = 1
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class DefaultConfig:
|
|
20
|
+
today: str
|
|
21
|
+
content: list
|
|
22
|
+
length: int
|
|
23
|
+
mode: Mode
|
|
24
|
+
api_key: str | None = None
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import random
|
|
3
|
+
from .constant import config, DATA_DIR, DefaultConfig
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def config_to_dict(cfg: DefaultConfig) -> dict:
|
|
7
|
+
return {
|
|
8
|
+
"today": cfg.today,
|
|
9
|
+
"content": cfg.content,
|
|
10
|
+
"length": cfg.length,
|
|
11
|
+
"mode": cfg.mode.value,
|
|
12
|
+
"api_key": cfg.api_key,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def write_config(cfg: DefaultConfig, preserve_api_key: bool = True) -> None:
|
|
17
|
+
if not DATA_DIR.exists():
|
|
18
|
+
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
|
19
|
+
|
|
20
|
+
if preserve_api_key and config.exists():
|
|
21
|
+
try:
|
|
22
|
+
existing = json.loads(config.read_text())
|
|
23
|
+
except (json.JSONDecodeError, FileNotFoundError):
|
|
24
|
+
existing = {}
|
|
25
|
+
|
|
26
|
+
# Update with new values, but only include api_key if it's not None
|
|
27
|
+
new_data = config_to_dict(cfg)
|
|
28
|
+
if cfg.api_key is None:
|
|
29
|
+
new_data.pop("api_key", None)
|
|
30
|
+
existing.update(new_data)
|
|
31
|
+
else:
|
|
32
|
+
existing = config_to_dict(cfg)
|
|
33
|
+
|
|
34
|
+
config.write_text(json.dumps(existing, indent=2))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def get_random_topics(length: int, n: int = 10) -> list[int]:
|
|
38
|
+
return [random.randint(0, length - 1) for _ in range(n)]
|
|
File without changes
|