text-effect-prompts 1.0.0__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.
@@ -0,0 +1,140 @@
1
+ """text-effect-prompts — AI text-effect prompts from FontVibe.
2
+
3
+ Code: MIT. Prompts and data: CC BY 4.0 — please credit with a link to the style's
4
+ FontVibe page (see :func:`attribution`). https://fontvibe.ai/styles
5
+
6
+ Same API and data as the npm package of the same name.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ from functools import lru_cache
12
+ from importlib import resources
13
+ from typing import Any, Dict, List, Optional
14
+
15
+ __all__ = [
16
+ "LANGS", "PLACEHOLDER", "stats", "all_styles", "get", "list_styles", "search",
17
+ "prompt", "name", "categories", "tags", "attribution",
18
+ ]
19
+ __version__ = "1.0.0"
20
+
21
+ LANGS = ("en", "ja", "zh", "es", "pt", "de")
22
+ PLACEHOLDER = "{{TEXT}}"
23
+ _LICENSE_URL = "https://creativecommons.org/licenses/by/4.0/"
24
+
25
+ Style = Dict[str, Any]
26
+
27
+
28
+ @lru_cache(maxsize=1)
29
+ def _index() -> Dict[str, Any]:
30
+ raw = resources.files(__package__).joinpath("index.json").read_text(encoding="utf-8")
31
+ return json.loads(raw)
32
+
33
+
34
+ def _norm(s: Any) -> str:
35
+ return ("" if s is None else str(s)).strip().lower()
36
+
37
+
38
+ def _take(items: List[Style], limit: Optional[int]) -> List[Style]:
39
+ # None / negative mean "no limit"; never slice with a negative number.
40
+ return items if limit is None or limit < 0 else items[:limit]
41
+
42
+
43
+ def stats() -> Dict[str, Any]:
44
+ """Counts for the published collection (styles, categories, exclusions...)."""
45
+ return _index()["stats"]
46
+
47
+
48
+ def all_styles() -> List[Style]:
49
+ """Every published style."""
50
+ return _index()["styles"]
51
+
52
+
53
+ def get(id_or_slug: str) -> Optional[Style]:
54
+ """Look a style up by id ("neon_glow") or slug ("neon-glow")."""
55
+ q = _norm(id_or_slug)
56
+ return next((s for s in all_styles() if s["id"] == q or s["slug"] == q), None)
57
+
58
+
59
+ def categories() -> List[str]:
60
+ """Category ids that have at least one style, in index order."""
61
+ seen: List[str] = []
62
+ for s in all_styles():
63
+ if s["category"] not in seen:
64
+ seen.append(s["category"])
65
+ return seen
66
+
67
+
68
+ def tags() -> List[Dict[str, Any]]:
69
+ """All tags with how many styles carry them, most common first."""
70
+ counts: Dict[str, int] = {}
71
+ for s in all_styles():
72
+ for t in s["tags"]:
73
+ counts[t] = counts.get(t, 0) + 1
74
+ return [{"tag": t, "count": c} for t, c in sorted(counts.items(), key=lambda kv: (-kv[1], kv[0]))]
75
+
76
+
77
+ def list_styles(category: Optional[str] = None, tag: Optional[str] = None, limit: Optional[int] = None) -> List[Style]:
78
+ """Filter by category and/or tag."""
79
+ cat, tg = _norm(category), _norm(tag)
80
+ return _take([s for s in all_styles() if (not cat or s["category"] == cat) and (not tg or tg in s["tags"])], limit)
81
+
82
+
83
+ def search(query: str, lang: Optional[str] = None, category: Optional[str] = None, limit: Optional[int] = None) -> List[Style]:
84
+ """Case-insensitive substring search over id, tags, and names and descriptions in
85
+ all six languages (or only ``lang``). Name and id matches rank first."""
86
+ q = _norm(query)
87
+ if not q:
88
+ return []
89
+ if lang and lang not in LANGS:
90
+ raise ValueError(f'text-effect-prompts: unknown lang "{lang}" (use one of {", ".join(LANGS)})')
91
+ langs = (lang,) if lang else LANGS
92
+ scored = []
93
+ for s in list_styles(category=category):
94
+ if q in s["id"] or q in s["slug"] or any(q in _norm(s["names"].get(l)) for l in langs):
95
+ score = 3
96
+ elif any(q in t for t in s["tags"]):
97
+ score = 2
98
+ elif any(q in _norm(s["description"].get(l)) for l in langs):
99
+ score = 1
100
+ else:
101
+ continue
102
+ scored.append((score, s))
103
+ scored.sort(key=lambda x: -x[0]) # stable: keeps index order within a score
104
+ return _take([s for _, s in scored], limit)
105
+
106
+
107
+ def prompt(id_or_slug: str, text: str) -> str:
108
+ """The standalone prompt for a style with {{TEXT}} replaced by your words."""
109
+ s = get(id_or_slug)
110
+ if s is None:
111
+ raise KeyError(f'text-effect-prompts: unknown style "{id_or_slug}"')
112
+ t = "" if text is None else str(text)
113
+ if not t.strip():
114
+ raise ValueError("text-effect-prompts: text must not be empty")
115
+ return s["prompt"].replace(PLACEHOLDER, t)
116
+
117
+
118
+ def name(id_or_slug: str, lang: Optional[str] = None) -> Optional[str]:
119
+ """Localised name, falling back to English when a translation is missing."""
120
+ s = get(id_or_slug)
121
+ if s is None:
122
+ return None
123
+ return (lang and s["names"].get(lang)) or s["names"]["en"]
124
+
125
+
126
+ def attribution(id_or_slug: Optional[str] = None, fmt: str = "markdown") -> str:
127
+ """A credit line for a style (or the whole collection when id is omitted).
128
+ ``fmt`` is "markdown", "html" or "text"."""
129
+ s = get(id_or_slug) if id_or_slug else None
130
+ if id_or_slug and s is None:
131
+ raise KeyError(f'text-effect-prompts: unknown style "{id_or_slug}"')
132
+ url = s["fontvibe_url"] if s else "https://fontvibe.ai/styles"
133
+ label = f'{s["names"]["en"]} prompt from FontVibe' if s else "Text-effect prompts from FontVibe"
134
+ if fmt == "html":
135
+ return label.replace("FontVibe", f'<a href="{url}">FontVibe</a>') + f' · <a href="{_LICENSE_URL}">CC BY 4.0</a>'
136
+ if fmt == "text":
137
+ return f"{label} — {url} — CC BY 4.0"
138
+ if fmt == "markdown":
139
+ return label.replace("FontVibe", f"[FontVibe]({url})") + f" · [CC BY 4.0]({_LICENSE_URL})"
140
+ raise ValueError("text-effect-prompts: fmt must be markdown, html or text")
@@ -0,0 +1,38 @@
1
+ """Command line: text-effect-prompts <style> <text> | --search <query> | --list [category]"""
2
+ import sys
3
+
4
+ from . import attribution, get, list_styles, prompt, search
5
+
6
+ USAGE = "\n".join([
7
+ "Usage:",
8
+ " text-effect-prompts <style> <text> print the prompt with your text filled in",
9
+ " text-effect-prompts --search <query> find styles (any of en ja zh es pt de)",
10
+ " text-effect-prompts --list [category] list styles",
11
+ "",
12
+ "Prompts: CC BY 4.0 — credit https://fontvibe.ai/styles/<style>. Best results on FontVibe.",
13
+ ])
14
+
15
+
16
+ def main(argv=None) -> int:
17
+ args = list(sys.argv[1:] if argv is None else argv)
18
+ if not args or args[0] in ("-h", "--help"):
19
+ print(USAGE)
20
+ return 0 if args else 1
21
+ if args[0] == "--search":
22
+ for s in search(" ".join(args[1:])):
23
+ print(s["slug"].ljust(28) + s["names"]["en"])
24
+ elif args[0] == "--list":
25
+ for s in list_styles(category=args[1] if len(args) > 1 else None):
26
+ print(s["slug"].ljust(28) + s["category"].ljust(13) + s["names"]["en"])
27
+ else:
28
+ s = get(args[0])
29
+ if s is None:
30
+ print(f'Unknown style "{args[0]}". Try --search.', file=sys.stderr)
31
+ return 1
32
+ print(prompt(s["id"], " ".join(args[1:]) or "FONTVIBE"))
33
+ print("\n# " + attribution(s["id"], "text"), file=sys.stderr)
34
+ return 0
35
+
36
+
37
+ if __name__ == "__main__":
38
+ sys.exit(main())