wordflow-cli 0.1.4__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.
- wordflow/__init__.py +1 -0
- wordflow/anki_flashcard.py +283 -0
- wordflow/cli.py +115 -0
- wordflow/clipboard.py +104 -0
- wordflow/configuration.py +171 -0
- wordflow/data/default_config.toml +103 -0
- wordflow/data/logo.txt +11 -0
- wordflow/data/logo2.txt +6 -0
- wordflow/my_classes.py +38 -0
- wordflow/notifications.py +77 -0
- wordflow/text_to_speech.py +49 -0
- wordflow/translator.py +170 -0
- wordflow/wizard.py +191 -0
- wordflow/workflows.py +146 -0
- wordflow_cli-0.1.4.dist-info/METADATA +195 -0
- wordflow_cli-0.1.4.dist-info/RECORD +20 -0
- wordflow_cli-0.1.4.dist-info/WHEEL +5 -0
- wordflow_cli-0.1.4.dist-info/entry_points.txt +2 -0
- wordflow_cli-0.1.4.dist-info/licenses/LICENSE +21 -0
- wordflow_cli-0.1.4.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# ==============================================================================
|
|
2
|
+
# Configuration for wordflow CLI
|
|
3
|
+
# Default Location: ~/.config/wordflow/config.toml
|
|
4
|
+
# ==============================================================================
|
|
5
|
+
# ------------------------------------------------------------------------------
|
|
6
|
+
[global]
|
|
7
|
+
# ISO 639-1 language codes (e.g., "en", "nl", "fr", "es")
|
|
8
|
+
# only exception is chinese: you can choose between zh-cn and zh-tw for simplified or traditional translation
|
|
9
|
+
# full list at: https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes
|
|
10
|
+
source_language = "auto" # translate from
|
|
11
|
+
target_language = "en" # translate to
|
|
12
|
+
|
|
13
|
+
enable_notifications = true # Send system notifications
|
|
14
|
+
|
|
15
|
+
# ------------------------------------------------------------------------------
|
|
16
|
+
# DEFAULT ANKI CONFIGURATION
|
|
17
|
+
# Used by default for all languages unless language-based overrides are specified
|
|
18
|
+
# ------------------------------------------------------------------------------
|
|
19
|
+
[anki]
|
|
20
|
+
[anki.default]
|
|
21
|
+
# default AnkiConnect URL
|
|
22
|
+
url = "http://localhost:8765"
|
|
23
|
+
|
|
24
|
+
# Target deck name.
|
|
25
|
+
# Supports dynamic placeholders: {source_language}, {target_language}
|
|
26
|
+
# Example: "Languages::{source_language}" creates cards in the deck: "Languages::es" if 'es' is your source_language
|
|
27
|
+
deck = "Languages::{source_language}"
|
|
28
|
+
|
|
29
|
+
# The name of the note model in Anki
|
|
30
|
+
card_model = "wordflow cloze"
|
|
31
|
+
|
|
32
|
+
# Map your personal field names
|
|
33
|
+
#field_names = ["front", "translation", "additional notes"]
|
|
34
|
+
|
|
35
|
+
# Allow duplicate cards if the same word/cloze already exists in the deck?
|
|
36
|
+
allow_duplicates = false
|
|
37
|
+
|
|
38
|
+
# Default tags automatically attached to every card added by wordflow
|
|
39
|
+
tags = ["wordflow", "auto-generated"]
|
|
40
|
+
|
|
41
|
+
# Audio: choose between none|word|sentence|both.
|
|
42
|
+
# Adds automatic audio recordings of the selection
|
|
43
|
+
audio_mode = "sentence"
|
|
44
|
+
# Dictionnary url
|
|
45
|
+
# if a dict_url scheme is specified, it will add a hyperlink to the cloze field
|
|
46
|
+
# dynamic placeholders are: {source_language} {target_language} {word}
|
|
47
|
+
# examples:
|
|
48
|
+
# dict_url=https://context.reverso.net/translation/{source_language}-{target_language}/{word}
|
|
49
|
+
# dict_url=https://en.wiktionary.org/wiki/{word}
|
|
50
|
+
dict_url = "https://glosbe.com/{source_language}/{target_language}/{word}"
|
|
51
|
+
|
|
52
|
+
# feel free to tweak those values if you use these fields
|
|
53
|
+
max_alternates = 3
|
|
54
|
+
max_definitions = 2
|
|
55
|
+
max_synonyms = 2
|
|
56
|
+
max_examples = 1
|
|
57
|
+
# ------------------------------------------------------------------------------
|
|
58
|
+
# FIELD MAPPINGS
|
|
59
|
+
# ------------------------------------------------------------------------------
|
|
60
|
+
# Map your personal Anki field names (left) to Wordflow data placeholders (right).
|
|
61
|
+
# You can combine multiple placeholders and use HTML tags like <br> or <b>.
|
|
62
|
+
# missing fields are automatically left blank
|
|
63
|
+
#
|
|
64
|
+
# Available placeholders:
|
|
65
|
+
# {cloze} - The sentence with the {{c1::word}} deletion
|
|
66
|
+
# {translation} - The translation of the full sentence
|
|
67
|
+
# {source_word} - The isolated highlighted word
|
|
68
|
+
# {word_audio}
|
|
69
|
+
# {sentence_audio}
|
|
70
|
+
# {sentence_phonetic} - Pinyin / Romaji / Phonetics (if available)
|
|
71
|
+
# {word_phonetic} - Pinyin / Romaji / Phonetics (if available)
|
|
72
|
+
# {alternates} - Alternative translations of the word (if available)
|
|
73
|
+
# {definitions} - Dictionary definitions of the word (if available)
|
|
74
|
+
# {synonyms} - Synonyms (if available)
|
|
75
|
+
# {examples} - Example sentences containing the word (if available)
|
|
76
|
+
[anki.default.fields]
|
|
77
|
+
"Sentence" = "{cloze}"
|
|
78
|
+
"Translation" = "{translation}"
|
|
79
|
+
"Audio" = "{word_audio} {sentence_audio}"
|
|
80
|
+
"Phonetics" = "{word_phonetic} {sentence_phonetic}"
|
|
81
|
+
"Definitions" = "{definitions}"
|
|
82
|
+
"Synonyms" = "{synonyms}"
|
|
83
|
+
"Alternates" = "{alternates}"
|
|
84
|
+
"Examples" = "{examples}"
|
|
85
|
+
# ------------------------------------------------------------------------------
|
|
86
|
+
# LANGUAGE-SPECIFIC OVERRIDES
|
|
87
|
+
# Triggered when the source language is detected, or when passing -l/--lang <language_iso_code>
|
|
88
|
+
# use it to change the deck, card model, fields, etc. dynamically based on detected language
|
|
89
|
+
# ------------------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
# Override for Chinese (-l zh-cn)
|
|
92
|
+
[anki.zh-cn]
|
|
93
|
+
deck = "Languages::Chinese"
|
|
94
|
+
|
|
95
|
+
# Override for Dutch (-l nl)
|
|
96
|
+
[anki.nl]
|
|
97
|
+
deck = "Languages::Nederlands"
|
|
98
|
+
|
|
99
|
+
[anki.es]
|
|
100
|
+
# experimental: change the accent by changing the top-level domain of the tts request. Please refer to https://gtts.readthedocs.io/en/latest/module.html#localized-accents for more info
|
|
101
|
+
audio_accent = "com.mx"
|
|
102
|
+
|
|
103
|
+
|
wordflow/data/logo.txt
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
$$\ $$\ $$\ $$$$$$$$\ $$\
|
|
2
|
+
$$ | $\ $$ | $$ | $$ _____|$$ |
|
|
3
|
+
$$ |$$$\ $$ | $$$$$$\ $$$$$$\ $$$$$$$ | $$ | $$ | $$$$$$\ $$\ $$\ $$\
|
|
4
|
+
$$ $$ $$\$$ |$$ __$$\ $$ __$$\ $$ __$$ | $$$$$\ $$ |$$ __$$\ $$ | $$ | $$ |
|
|
5
|
+
$$$$ _$$$$ |$$ / $$ |$$ | \__|$$ / $$ | $$ __| $$ |$$ / $$ |$$ | $$ | $$ |
|
|
6
|
+
$$$ / \$$$ |$$ | $$ |$$ | $$ | $$ | $$ | $$ |$$ | $$ |$$ | $$ | $$ |
|
|
7
|
+
$$ / \$$ |\$$$$$$ |$$ | \$$$$$$$ | $$ | $$ |\$$$$$$ |\$$$$$\$$$$ |
|
|
8
|
+
\__/ \__| \______/ \__| \_______| \__| \__| \______/ \_____\____/
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
wordflow/data/logo2.txt
ADDED
wordflow/my_classes.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@dataclass
|
|
5
|
+
class AnkiConfig:
|
|
6
|
+
url: str
|
|
7
|
+
deck: str
|
|
8
|
+
card_model: str
|
|
9
|
+
tags: list[str]
|
|
10
|
+
dict_url: str | None
|
|
11
|
+
allow_duplicates: bool
|
|
12
|
+
fields: dict
|
|
13
|
+
audio_mode: str = "none"
|
|
14
|
+
audio_accent: str = "com"
|
|
15
|
+
max_synonyms: int = 3
|
|
16
|
+
max_alternates: int = 3
|
|
17
|
+
max_definitions: int = 2
|
|
18
|
+
max_examples: int = 1
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class GlobalConfig:
|
|
23
|
+
source_language: str
|
|
24
|
+
target_language: str
|
|
25
|
+
enable_notifications: bool = True
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class TranslationData:
|
|
30
|
+
source_data: str
|
|
31
|
+
translated_data: str
|
|
32
|
+
source_language: str
|
|
33
|
+
target_language: str
|
|
34
|
+
phonetic: str | None
|
|
35
|
+
alternate_translations: list[str] | None
|
|
36
|
+
synonyms: list[str] | None
|
|
37
|
+
definitions: list[str] | None
|
|
38
|
+
examples: list[str] | None
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import shutil
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from wordflow.clipboard import copy_to_clipboard
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def notify(
|
|
9
|
+
title: str,
|
|
10
|
+
message: str,
|
|
11
|
+
urgency: str = "normal",
|
|
12
|
+
timeout: int = 5000,
|
|
13
|
+
enable_notifications=True,
|
|
14
|
+
copy_output_to_clipboard=False,
|
|
15
|
+
):
|
|
16
|
+
"""
|
|
17
|
+
Sends a Linux desktop notification using notify-send, or simply prints message to terminal
|
|
18
|
+
"""
|
|
19
|
+
if not enable_notifications:
|
|
20
|
+
print(title, "\n", message)
|
|
21
|
+
return
|
|
22
|
+
|
|
23
|
+
# Check if notify-send is installed on the user's system
|
|
24
|
+
if not shutil.which("notify-send"):
|
|
25
|
+
print(
|
|
26
|
+
f"[Warning] 'notify-send' is not installed. Missing notification: {title}",
|
|
27
|
+
file=sys.stderr,
|
|
28
|
+
)
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
if not copy_output_to_clipboard:
|
|
33
|
+
subprocess.run(
|
|
34
|
+
[
|
|
35
|
+
"notify-send",
|
|
36
|
+
"-a",
|
|
37
|
+
"Wordflow", # App name shown in notification center
|
|
38
|
+
"-u",
|
|
39
|
+
urgency, # Urgency: low, normal, critical
|
|
40
|
+
"-t",
|
|
41
|
+
str(timeout), # Timeout in milliseconds
|
|
42
|
+
title,
|
|
43
|
+
message,
|
|
44
|
+
],
|
|
45
|
+
check=True,
|
|
46
|
+
capture_output=True,
|
|
47
|
+
text=True,
|
|
48
|
+
)
|
|
49
|
+
else:
|
|
50
|
+
process = subprocess.run(
|
|
51
|
+
[
|
|
52
|
+
"notify-send",
|
|
53
|
+
"-a",
|
|
54
|
+
"Wordflow",
|
|
55
|
+
"--action=default=Copy",
|
|
56
|
+
"--wait",
|
|
57
|
+
"-u",
|
|
58
|
+
urgency,
|
|
59
|
+
"-t",
|
|
60
|
+
str(timeout),
|
|
61
|
+
title,
|
|
62
|
+
message,
|
|
63
|
+
],
|
|
64
|
+
check=True,
|
|
65
|
+
capture_output=True,
|
|
66
|
+
text=True,
|
|
67
|
+
)
|
|
68
|
+
if process.stdout.strip() == "default":
|
|
69
|
+
# Push the translation to the Wayland clipboard
|
|
70
|
+
copy_to_clipboard(message)
|
|
71
|
+
|
|
72
|
+
except subprocess.CalledProcessError as e:
|
|
73
|
+
print(
|
|
74
|
+
f"[Warning] Notification daemon error: {e.stderr.strip()}", file=sys.stderr
|
|
75
|
+
)
|
|
76
|
+
except Exception as e:
|
|
77
|
+
print(f"[Warning] Failed to send notification '{title}': {e}", file=sys.stderr)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import requests
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class AudioError(Exception):
|
|
6
|
+
"""Custom exception raised when TTS generation fails."""
|
|
7
|
+
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_audio(text: str, language: str, accent: str = ""):
|
|
12
|
+
"""
|
|
13
|
+
Fetches TTS audio directly from Google's widget API.
|
|
14
|
+
Returns a base64 UTF-8 encoded string ready for AnkiConnect.
|
|
15
|
+
|
|
16
|
+
:param text: The text to be spoken.
|
|
17
|
+
:param source_language: ISO code (e.g., 'en', 'fr', 'zh').
|
|
18
|
+
:param accent: Optional TLD (e.g., 'co.uk', 'com.au', 'ca') to change the accent.
|
|
19
|
+
"""
|
|
20
|
+
text_to_speak = text.strip()
|
|
21
|
+
if not text_to_speak:
|
|
22
|
+
return ""
|
|
23
|
+
# Safety mechanism: Google's TTS endpoint has a strict 200-character limit.
|
|
24
|
+
# Truncate to prevent HTTP 400 Bad Request errors on massive clipboard copies.
|
|
25
|
+
if len(text_to_speak) > 200:
|
|
26
|
+
text_to_speak = text_to_speak[:200]
|
|
27
|
+
# Accent trick
|
|
28
|
+
tld = accent.strip() if accent else "com"
|
|
29
|
+
url = f"https://translate.google.{tld}/translate_tts"
|
|
30
|
+
params = {
|
|
31
|
+
"ie": "UTF-8",
|
|
32
|
+
"q": text_to_speak,
|
|
33
|
+
"tl": language,
|
|
34
|
+
"client": "tw-ob", # Bypasses the token generator
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"}
|
|
38
|
+
try:
|
|
39
|
+
response = requests.get(url, params=params, headers=headers, timeout=5)
|
|
40
|
+
response.raise_for_status()
|
|
41
|
+
|
|
42
|
+
audio_bytes = response.content
|
|
43
|
+
|
|
44
|
+
audio_b64 = base64.b64encode(audio_bytes).decode("utf-8")
|
|
45
|
+
|
|
46
|
+
return audio_b64
|
|
47
|
+
|
|
48
|
+
except requests.RequestException as e:
|
|
49
|
+
raise AudioError(f"Failed to fetch audio from Google TTS: {str(e)}")
|
wordflow/translator.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
from time import sleep
|
|
3
|
+
|
|
4
|
+
from wordflow.my_classes import TranslationData
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TranslationError(Exception):
|
|
8
|
+
"""Custom exception raised when translation fails to prevent bad Anki cards."""
|
|
9
|
+
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def normalize_lang_code(google_code: str) -> str:
|
|
14
|
+
"""Converts Google's legacy or dialect-specific codes into ISO 639-1."""
|
|
15
|
+
if not google_code:
|
|
16
|
+
return ""
|
|
17
|
+
|
|
18
|
+
code = google_code.lower()
|
|
19
|
+
|
|
20
|
+
legacy_map = {
|
|
21
|
+
"iw": "he", # Hebrew
|
|
22
|
+
"in": "id", # Indonesian
|
|
23
|
+
"jw": "jv", # Javanese
|
|
24
|
+
"ji": "yi", # Yiddish
|
|
25
|
+
}
|
|
26
|
+
if code in legacy_map:
|
|
27
|
+
return legacy_map[code]
|
|
28
|
+
|
|
29
|
+
if code in ["zh-cn", "zh-tw"]:
|
|
30
|
+
return code
|
|
31
|
+
|
|
32
|
+
# 2. Strip regional subtags (e.g., 'zh-cn' -> 'zh', 'en-us' -> 'en')
|
|
33
|
+
if "-" in code:
|
|
34
|
+
code = code.split("-")[0]
|
|
35
|
+
|
|
36
|
+
return code
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def translate(
|
|
40
|
+
text: str,
|
|
41
|
+
source_language: str = "auto",
|
|
42
|
+
target_language: str = "en",
|
|
43
|
+
max_retries: int = 3,
|
|
44
|
+
) -> TranslationData:
|
|
45
|
+
"""
|
|
46
|
+
Takes text in any language and translates it into target_language.
|
|
47
|
+
Returns a dictionnary containing the translation, source language used, and additional info in case of singe word translation: pronounciation, alternate translations, definition, synonyms, examples
|
|
48
|
+
"""
|
|
49
|
+
text_to_translate = text.strip()
|
|
50
|
+
if not text_to_translate:
|
|
51
|
+
raise TranslationError("No text was provided for translation.")
|
|
52
|
+
|
|
53
|
+
url = "https://translate.googleapis.com/translate_a/single"
|
|
54
|
+
params = {
|
|
55
|
+
"client": "gtx", # The magical bypass client
|
|
56
|
+
"sl": source_language, # (auto is supported)
|
|
57
|
+
"tl": target_language,
|
|
58
|
+
"q": text, # The text to translate
|
|
59
|
+
"dt": [
|
|
60
|
+
"t",
|
|
61
|
+
"bd",
|
|
62
|
+
"rm",
|
|
63
|
+
"md",
|
|
64
|
+
"ss",
|
|
65
|
+
"ex",
|
|
66
|
+
], # Request the "translation", pronounciation, alternate translations, definition, synonyms, and examples
|
|
67
|
+
}
|
|
68
|
+
headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"}
|
|
69
|
+
|
|
70
|
+
last_error = None
|
|
71
|
+
|
|
72
|
+
for _ in range(max_retries):
|
|
73
|
+
try:
|
|
74
|
+
response = requests.get(url, params=params, headers=headers, timeout=5)
|
|
75
|
+
# FAIL FAST: If Google rate-limits, abort instantly to avoid extending the ban.
|
|
76
|
+
if response.status_code == 429:
|
|
77
|
+
raise TranslationError(
|
|
78
|
+
"Google temporarily rate-limited your IP (HTTP 429). Please wait a few minutes."
|
|
79
|
+
)
|
|
80
|
+
# other errors
|
|
81
|
+
response.raise_for_status()
|
|
82
|
+
|
|
83
|
+
data = parse_translation_data(response.json())
|
|
84
|
+
translation_data = TranslationData(
|
|
85
|
+
source_data=text,
|
|
86
|
+
translated_data=data["translation"],
|
|
87
|
+
source_language=data["source_language"],
|
|
88
|
+
target_language=target_language,
|
|
89
|
+
phonetic=data.get("phonetic"),
|
|
90
|
+
alternate_translations=data.get("alternates", ""),
|
|
91
|
+
synonyms=data.get("synonyms"),
|
|
92
|
+
definitions=data.get("definitions"),
|
|
93
|
+
examples=data.get("examples"),
|
|
94
|
+
)
|
|
95
|
+
# DEBUG
|
|
96
|
+
print("json data: ", response.json())
|
|
97
|
+
print("translation data: ", translation_data)
|
|
98
|
+
return translation_data
|
|
99
|
+
|
|
100
|
+
except requests.RequestException as e:
|
|
101
|
+
last_error = e
|
|
102
|
+
sleep(1) # wait ane retry
|
|
103
|
+
|
|
104
|
+
raise TranslationError(f"Failed to translate. Last error: {last_error}")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def parse_translation_data(data) -> dict:
|
|
108
|
+
# parse TRANSLATION
|
|
109
|
+
translation = "".join([sentence[0] for sentence in data[0] if sentence[0]])
|
|
110
|
+
# parse PHONETIC
|
|
111
|
+
phonetic = ""
|
|
112
|
+
for item in data[0]:
|
|
113
|
+
# Romanizations are placed in the 3rd index of a specific sub-array
|
|
114
|
+
if len(item) > 3 and item[3]:
|
|
115
|
+
phonetic = item[3]
|
|
116
|
+
break
|
|
117
|
+
# 3. parse DICTIONARY (Alternate translations)
|
|
118
|
+
# data[1] exists if it's a single word and dt=bd was requested
|
|
119
|
+
alternates = []
|
|
120
|
+
if len(data) > 1 and data[1]:
|
|
121
|
+
for pos_group in data[1]:
|
|
122
|
+
# pos_group[0] is the part of speech (e.g., "noun")
|
|
123
|
+
# pos_group[1] is a list of translated words
|
|
124
|
+
if len(pos_group) > 1:
|
|
125
|
+
alternates.extend(pos_group[1])
|
|
126
|
+
# SOURCE LANGUAGE
|
|
127
|
+
detected_source_language = data[2]
|
|
128
|
+
iso_source_language = normalize_lang_code(detected_source_language)
|
|
129
|
+
|
|
130
|
+
definitions = []
|
|
131
|
+
synonyms = []
|
|
132
|
+
examples = []
|
|
133
|
+
# 4. parse SYNONYMS
|
|
134
|
+
## Located at data[11]. Structure: [ ["noun", [ [ ["syn1", "syn2"], "id" ] ] ], ... ]
|
|
135
|
+
if len(data) > 11 and data[11]:
|
|
136
|
+
for pos_group in data[11]:
|
|
137
|
+
if len(pos_group) > 1 and isinstance(pos_group[1], list):
|
|
138
|
+
for entry in pos_group[1]:
|
|
139
|
+
if (
|
|
140
|
+
isinstance(entry, list)
|
|
141
|
+
and len(entry) > 0
|
|
142
|
+
and isinstance(entry[0], list)
|
|
143
|
+
):
|
|
144
|
+
synonyms.extend(entry[0])
|
|
145
|
+
|
|
146
|
+
# 5. parse DEFINITIONS
|
|
147
|
+
# Located at data[12]. Structure: [ ["noun", [ ["definition1", "id", "example"], ... ] ], ... ]
|
|
148
|
+
if len(data) > 12 and data[12]:
|
|
149
|
+
for pos_group in data[12]:
|
|
150
|
+
if len(pos_group) > 1 and isinstance(pos_group[1], list):
|
|
151
|
+
for entry in pos_group[1]:
|
|
152
|
+
if isinstance(entry, list) and len(entry) > 0:
|
|
153
|
+
definitions.append(entry[0])
|
|
154
|
+
|
|
155
|
+
# 6. parse EXAMPLES
|
|
156
|
+
# Located at data[13]. Structure: [ [ ["example 1 with <b>tags</b>", ...], ... ] ]
|
|
157
|
+
if len(data) > 13 and data[13]:
|
|
158
|
+
if isinstance(data[13], list) and len(data[13]) > 0:
|
|
159
|
+
for ex_group in data[13][0]:
|
|
160
|
+
if isinstance(ex_group, list) and len(ex_group) > 0:
|
|
161
|
+
examples.append(str(ex_group[0]))
|
|
162
|
+
return {
|
|
163
|
+
"translation": translation,
|
|
164
|
+
"phonetic": phonetic,
|
|
165
|
+
"alternates": list(set(alternates)),
|
|
166
|
+
"definitions": definitions,
|
|
167
|
+
"synonyms": list(set(synonyms)),
|
|
168
|
+
"examples": examples,
|
|
169
|
+
"source_language": iso_source_language,
|
|
170
|
+
}
|
wordflow/wizard.py
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from importlib.resources import files
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _prompt(text: str, default: str) -> str:
|
|
7
|
+
"""Helper to prompt for a string with a fallback default."""
|
|
8
|
+
ans = input(f"{text} [{default}]: ").strip()
|
|
9
|
+
return ans if ans else default
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _prompt_bool(text: str, default: bool) -> bool:
|
|
13
|
+
"""Helper to prompt for a boolean (y/n)."""
|
|
14
|
+
default_str = "Y/n" if default else "y/N"
|
|
15
|
+
ans = input(f"{text} [{default_str}]: ").strip().lower()
|
|
16
|
+
if not ans:
|
|
17
|
+
return default
|
|
18
|
+
return ans in ("y", "yes", "true")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _prompt_int(text: str, default: int) -> int:
|
|
22
|
+
"""Helper to prompt for an integer."""
|
|
23
|
+
while True:
|
|
24
|
+
ans = input(f"{text} [{default}]: ").strip()
|
|
25
|
+
if not ans:
|
|
26
|
+
return default
|
|
27
|
+
if ans.isdigit():
|
|
28
|
+
return int(ans)
|
|
29
|
+
print("[-] Invalid input. Please type a valid number.\n")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _prompt_list(text: str, default: list[str]) -> list[str]:
|
|
33
|
+
"""Helper to prompt for a comma-separated list."""
|
|
34
|
+
default_str = ", ".join(default)
|
|
35
|
+
ans = input(f"{text} [{default_str}]: ").strip()
|
|
36
|
+
if not ans:
|
|
37
|
+
return default
|
|
38
|
+
return [item.strip() for item in ans.split(",") if item.strip()]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _prompt_numbered_choice(text: str, choices: list[str], default: str) -> str:
|
|
42
|
+
"""Prompts the user to choose from a numbered list of options."""
|
|
43
|
+
print(f"{text} [Default: {default}]")
|
|
44
|
+
|
|
45
|
+
for i, choice in enumerate(choices, 1):
|
|
46
|
+
print(f" {i}. {choice}")
|
|
47
|
+
|
|
48
|
+
while True:
|
|
49
|
+
ans = input("Select a number (or press ENTER for default): ").strip()
|
|
50
|
+
|
|
51
|
+
if not ans:
|
|
52
|
+
return default
|
|
53
|
+
|
|
54
|
+
if ans.isdigit():
|
|
55
|
+
idx = int(ans)
|
|
56
|
+
if 1 <= idx <= len(choices):
|
|
57
|
+
return choices[idx - 1]
|
|
58
|
+
|
|
59
|
+
for choice in choices:
|
|
60
|
+
if ans.lower() == choice.lower():
|
|
61
|
+
return choice
|
|
62
|
+
|
|
63
|
+
print(
|
|
64
|
+
f"[-] Invalid input. Please type a number between 1 and {len(choices)}.\n"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _display_ascii_art():
|
|
69
|
+
"""Attempts to load and print the ASCII art logo from the package data."""
|
|
70
|
+
try:
|
|
71
|
+
art = files("wordflow").joinpath("data", "logo.txt").read_text(encoding="utf-8")
|
|
72
|
+
print(f"\033[94m{art}\033[0m")
|
|
73
|
+
except Exception:
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def launch_wizard() -> dict:
|
|
78
|
+
"""
|
|
79
|
+
Launches an interactive command-line wizard to configure Wordflow.
|
|
80
|
+
Returns a dictionary structured for the config.toml file.
|
|
81
|
+
"""
|
|
82
|
+
config_path = Path(os.path.expanduser("~/.config/wordflow/config.toml"))
|
|
83
|
+
|
|
84
|
+
_display_ascii_art()
|
|
85
|
+
print("=" * 60)
|
|
86
|
+
print(" Welcome to the Wordflow Setup Wizard!")
|
|
87
|
+
print(" This will generate your configuration file at:")
|
|
88
|
+
print(f" {config_path}")
|
|
89
|
+
print(" Press ENTER to accept the [default] values.")
|
|
90
|
+
print("=" * 60 + "\n")
|
|
91
|
+
|
|
92
|
+
# --- GLOBAL ---
|
|
93
|
+
print("--- 1. Language & Global Settings ---")
|
|
94
|
+
source_language = _prompt(
|
|
95
|
+
"Source language (translate from) [auto, en, zh-cn...]", "auto"
|
|
96
|
+
)
|
|
97
|
+
target_language = _prompt("Target language (translate to) [en, fr, nl...]", "en")
|
|
98
|
+
enable_notifs = _prompt_bool("Enable desktop notifications?", True)
|
|
99
|
+
print()
|
|
100
|
+
|
|
101
|
+
# --- ANKI ---
|
|
102
|
+
print("--- 2. Anki Settings ---")
|
|
103
|
+
print("Make sure Anki is open with AnkiConnect installed.")
|
|
104
|
+
anki_url = _prompt("AnkiConnect URL", "http://localhost:8765")
|
|
105
|
+
deck = _prompt(
|
|
106
|
+
"Default Deck name (use {source_language} for dynamic)",
|
|
107
|
+
"Languages::{source_language}",
|
|
108
|
+
)
|
|
109
|
+
card_model = _prompt("Default Card Model name", "wordflow cloze")
|
|
110
|
+
|
|
111
|
+
# Inform the user about the new dictionary-based field layout instead of asking them to type it
|
|
112
|
+
print(
|
|
113
|
+
"\n -> Wordflow will automatically configure a rich 8-field layout (Sentence, Translation, Audio, Dictionary, etc.)."
|
|
114
|
+
)
|
|
115
|
+
print(
|
|
116
|
+
" -> You can easily customize these mappings or add HTML later in the config file.\n"
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
tags = _prompt_list(
|
|
120
|
+
"Default tags to apply to cards", ["wordflow", "auto-generated"]
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
allow_duplicates = _prompt_bool("Allow duplicate cards?", False)
|
|
124
|
+
|
|
125
|
+
audio_mode = _prompt_numbered_choice(
|
|
126
|
+
"Audio generation mode", ["none", "word", "sentence", "both"], "sentence"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
add_dic_url = _prompt_bool(
|
|
130
|
+
"Add automatic dictionary hyperlinks to flashcards?", False
|
|
131
|
+
)
|
|
132
|
+
if add_dic_url:
|
|
133
|
+
dict_url = _prompt(
|
|
134
|
+
"Dictionary URL. Supported dynamic tags: {source_language}, {target_language}, {word}",
|
|
135
|
+
"https://glosbe.com/{source_language}/{target_language}/{word}",
|
|
136
|
+
)
|
|
137
|
+
else:
|
|
138
|
+
dict_url = ""
|
|
139
|
+
|
|
140
|
+
# --- DICTIONARY DATA ---
|
|
141
|
+
print("--- 3. Dictionary & Card Richness ---")
|
|
142
|
+
print("Set the maximum number of items to display on your cards if available.")
|
|
143
|
+
max_alternates = _prompt_int("Max alternate translations", 3)
|
|
144
|
+
max_definitions = _prompt_int("Max monolingual definitions", 2)
|
|
145
|
+
max_synonyms = _prompt_int("Max synonyms", 2)
|
|
146
|
+
max_examples = _prompt_int("Max example sentences", 1)
|
|
147
|
+
|
|
148
|
+
print("\n" + "=" * 60)
|
|
149
|
+
print(" Configuration complete!")
|
|
150
|
+
print(
|
|
151
|
+
" Note: You can tweak dictionary limits, field mappings, and language-specific"
|
|
152
|
+
)
|
|
153
|
+
print(f" overrides by editing {config_path} manually later.")
|
|
154
|
+
print("=" * 60)
|
|
155
|
+
|
|
156
|
+
# Construct the dictionary perfectly matching the new TOML schema
|
|
157
|
+
return {
|
|
158
|
+
"global": {
|
|
159
|
+
"source_language": source_language,
|
|
160
|
+
"target_language": target_language,
|
|
161
|
+
"enable_notifications": enable_notifs,
|
|
162
|
+
},
|
|
163
|
+
"anki": {
|
|
164
|
+
"default": {
|
|
165
|
+
"url": anki_url,
|
|
166
|
+
"deck": deck,
|
|
167
|
+
"card_model": card_model,
|
|
168
|
+
"allow_duplicates": allow_duplicates,
|
|
169
|
+
"tags": tags,
|
|
170
|
+
"audio_mode": audio_mode,
|
|
171
|
+
"dict_url": dict_url,
|
|
172
|
+
"max_alternates": 3,
|
|
173
|
+
"max_definitions": 2,
|
|
174
|
+
"max_synonyms": 2,
|
|
175
|
+
"max_examples": 1,
|
|
176
|
+
"fields": {
|
|
177
|
+
"Sentence": "{cloze}",
|
|
178
|
+
"Translation": "{translation}",
|
|
179
|
+
"Audio": "{word_audio} {sentence_audio}",
|
|
180
|
+
"Phonetics": "{word_phonetic} {sentence_phonetic}",
|
|
181
|
+
"Definitions": "{definitions}",
|
|
182
|
+
"Synonyms": "{synonyms}",
|
|
183
|
+
"Alternates": "{alternates}",
|
|
184
|
+
"Examples": "{examples}",
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
# Examples of override
|
|
188
|
+
"nl": {"deck": "Languages::Nederlands"},
|
|
189
|
+
"es": {"audio_accent": "com.mx"},
|
|
190
|
+
},
|
|
191
|
+
}
|