chore: auto-commit from dashboard
This commit is contained in:
@@ -148,6 +148,15 @@ def create_bot(config: Config) -> discord.Client:
|
||||
"**Reminders**",
|
||||
"`/remind <time> <text> [date]` — Create reminder",
|
||||
"",
|
||||
"**Financiar (roa2web)**",
|
||||
"`/sold [firma]` — Sold casă + bancă",
|
||||
"`/trezorerie [firma]` — Detaliu pe conturi",
|
||||
"`/facturi [firma]` — Facturi neîncasate (top sold)",
|
||||
"`/firme [nume]` — Listă firme roa2web",
|
||||
"",
|
||||
"**Vremea**",
|
||||
"`/vremea [oras]` — Vremea curentă (implicit Constanța)",
|
||||
"",
|
||||
"**Git**",
|
||||
"`/commit [message]` — Commit all changes",
|
||||
"`/push` — Push to remote",
|
||||
@@ -564,6 +573,50 @@ def create_bot(config: Config) -> discord.Client:
|
||||
result = await asyncio.to_thread(fast_dispatch, "remind", args)
|
||||
await interaction.followup.send(result, ephemeral=True)
|
||||
|
||||
# --- Financiar (roa2web) ---
|
||||
|
||||
@tree.command(name="sold", description="Sold casă + bancă (roa2web)")
|
||||
@app_commands.describe(firma="Cod sau nume firmă (implicit: Romfast)")
|
||||
async def sold_cmd(interaction: discord.Interaction, firma: str | None = None) -> None:
|
||||
await interaction.response.defer(ephemeral=True)
|
||||
args = firma.split() if firma else []
|
||||
result = await asyncio.to_thread(fast_dispatch, "sold", args)
|
||||
await interaction.followup.send(result, ephemeral=True)
|
||||
|
||||
@tree.command(name="trezorerie", description="Trezorerie detaliată pe conturi (roa2web)")
|
||||
@app_commands.describe(firma="Cod sau nume firmă (implicit: Romfast)")
|
||||
async def trezorerie_cmd(interaction: discord.Interaction, firma: str | None = None) -> None:
|
||||
await interaction.response.defer(ephemeral=True)
|
||||
args = firma.split() if firma else []
|
||||
result = await asyncio.to_thread(fast_dispatch, "trezorerie", args)
|
||||
await interaction.followup.send(result, ephemeral=True)
|
||||
|
||||
@tree.command(name="facturi", description="Facturi neîncasate, top după sold (roa2web)")
|
||||
@app_commands.describe(firma="Cod sau nume firmă (implicit: Romfast)")
|
||||
async def facturi_cmd(interaction: discord.Interaction, firma: str | None = None) -> None:
|
||||
await interaction.response.defer(ephemeral=True)
|
||||
args = firma.split() if firma else []
|
||||
result = await asyncio.to_thread(fast_dispatch, "facturi", args)
|
||||
await interaction.followup.send(result, ephemeral=True)
|
||||
|
||||
@tree.command(name="firme", description="Listă firme roa2web, opțional filtrate după nume")
|
||||
@app_commands.describe(nume="Filtru după nume (opțional)")
|
||||
async def firme_cmd(interaction: discord.Interaction, nume: str | None = None) -> None:
|
||||
await interaction.response.defer()
|
||||
args = nume.split() if nume else []
|
||||
result = await asyncio.to_thread(fast_dispatch, "firme", args)
|
||||
await interaction.followup.send(result)
|
||||
|
||||
# --- Vremea ---
|
||||
|
||||
@tree.command(name="vremea", description="Vremea curentă (implicit Constanța)")
|
||||
@app_commands.describe(oras="Oraș (opțional, implicit Constanța)")
|
||||
async def vremea_cmd(interaction: discord.Interaction, oras: str | None = None) -> None:
|
||||
await interaction.response.defer()
|
||||
args = oras.split() if oras else []
|
||||
result = await asyncio.to_thread(fast_dispatch, "vremea", args)
|
||||
await interaction.followup.send(result)
|
||||
|
||||
# --- Git commands ---
|
||||
|
||||
@tree.command(name="commit", description="Git commit all changes")
|
||||
@@ -776,9 +829,9 @@ def create_bot(config: Config) -> discord.Client:
|
||||
if cost > 0:
|
||||
tokens_line += f" | ${cost:.4f}"
|
||||
|
||||
# Context window usage
|
||||
# Context window usage (Sonnet/Opus: 1M, Haiku: 200K)
|
||||
ctx = session.get("context_tokens", 0)
|
||||
max_ctx = 200_000
|
||||
max_ctx = 200_000 if model == "haiku" else 1_000_000
|
||||
pct = round(ctx / max_ctx * 100) if ctx else 0
|
||||
context_line = f"Context: {_fmt_tokens(ctx)}/{_fmt_tokens(max_ctx)} ({pct}%)"
|
||||
|
||||
@@ -1155,10 +1208,13 @@ def create_bot(config: Config) -> discord.Client:
|
||||
|
||||
@client.event
|
||||
async def on_ready() -> None:
|
||||
# Sync to each guild instantly, then global (global can take up to 1h)
|
||||
# Sync guild-scoped copies instantly, then clear+sync the global
|
||||
# scope so it stays empty — leaving both registered makes every
|
||||
# command show up twice in Discord's autocomplete UI.
|
||||
for guild in client.guilds:
|
||||
tree.copy_global_to(guild=guild)
|
||||
await tree.sync(guild=guild)
|
||||
tree.clear_commands(guild=None)
|
||||
await tree.sync()
|
||||
scheduler = getattr(client, "scheduler", None)
|
||||
if scheduler is not None:
|
||||
|
||||
@@ -14,6 +14,8 @@ from datetime import datetime, date, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Callable
|
||||
|
||||
import requests
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||||
@@ -559,6 +561,179 @@ def cmd_remind(args: list[str]) -> str:
|
||||
sys.path.remove(str(TOOLS_DIR))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Financiar (roa2web)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _roa2web():
|
||||
from tools.roa2web_client import ROA2WebClient, resolve_company, ROA2WebError
|
||||
return ROA2WebClient(), resolve_company, ROA2WebError
|
||||
|
||||
|
||||
def _fmt_lei(n: float) -> str:
|
||||
"""Rotunjit la sute, fără zecimale — convenție stabilită cu Marius."""
|
||||
n = round(float(n) / 100) * 100
|
||||
return f"{n:,.0f}".replace(",", ".")
|
||||
|
||||
|
||||
_RO_MONTHS = [
|
||||
"", "ianuarie", "februarie", "martie", "aprilie", "mai", "iunie",
|
||||
"iulie", "august", "septembrie", "octombrie", "noiembrie", "decembrie",
|
||||
]
|
||||
|
||||
|
||||
def _company_name(client, company: str) -> str:
|
||||
for c in client.companies():
|
||||
if str(c["id_firma"]) == str(company):
|
||||
return c["name"]
|
||||
return f"firma {company}"
|
||||
|
||||
|
||||
def _accounting_period_label(client, company: str) -> str:
|
||||
"""Luna contabilă curentă (an/lună) — dedusă din /reports/invoices."""
|
||||
try:
|
||||
period = client.get("/reports/invoices", {"company": company, "page_size": 1}).get("accounting_period") or {}
|
||||
except Exception:
|
||||
return ""
|
||||
luna, an = period.get("luna"), period.get("an")
|
||||
if not luna or not an:
|
||||
return ""
|
||||
return f"{_RO_MONTHS[luna]} {an}"
|
||||
|
||||
|
||||
def _header(name: str, period: str) -> str:
|
||||
return f"{name} — {period}" if period else name
|
||||
|
||||
|
||||
def cmd_sold(args: list[str]) -> str:
|
||||
"""Sold casă + bancă (roa2web). Args: [firmă] (cod sau nume parțial)."""
|
||||
client, resolve_company, ROA2WebError = _roa2web()
|
||||
try:
|
||||
company = resolve_company(client, " ".join(args) if args else None)
|
||||
data = client.dashboard_summary(company=company)
|
||||
header = _header(_company_name(client, company), _accounting_period_label(client, company))
|
||||
except ROA2WebError as e:
|
||||
return f"Eroare roa2web: {e}"
|
||||
accounts = data.get("treasury_accounts", [])
|
||||
casa = sum(a["sold"] for a in accounts if a.get("cont") == "5311")
|
||||
banca = sum(a["sold"] for a in accounts if a.get("cont") == "5121")
|
||||
return f"{header}\nSold casă: ~{_fmt_lei(casa)} lei\nSold bancă: ~{_fmt_lei(banca)} lei"
|
||||
|
||||
|
||||
def cmd_trezorerie(args: list[str]) -> str:
|
||||
"""Trezorerie detaliată pe conturi (roa2web). Args: [firmă]."""
|
||||
client, resolve_company, ROA2WebError = _roa2web()
|
||||
try:
|
||||
company = resolve_company(client, " ".join(args) if args else None)
|
||||
data = client.dashboard_summary(company=company)
|
||||
header = _header(_company_name(client, company), _accounting_period_label(client, company))
|
||||
except ROA2WebError as e:
|
||||
return f"Eroare roa2web: {e}"
|
||||
accounts = data.get("treasury_accounts", [])
|
||||
if not accounts:
|
||||
return f"{header}\nNiciun cont de trezorerie găsit."
|
||||
lines = [f" {a.get('nume_banca') or a.get('nume_cont', '?')}: ~{_fmt_lei(a['sold'])} lei" for a in accounts]
|
||||
totals = data.get("treasury_totals_by_currency", {})
|
||||
total_line = ", ".join(f"~{_fmt_lei(v)} {cur}" for cur, v in totals.items()) or "n/a"
|
||||
return f"{header}\nTrezorerie:\n" + "\n".join(lines) + f"\nTotal: {total_line}"
|
||||
|
||||
|
||||
def cmd_facturi(args: list[str]) -> str:
|
||||
"""Facturi neîncasate, top după sold (roa2web). Args: [firmă]."""
|
||||
client, resolve_company, ROA2WebError = _roa2web()
|
||||
try:
|
||||
company = resolve_company(client, " ".join(args) if args else None)
|
||||
data = client.get("/reports/invoices", {"company": company})
|
||||
name = _company_name(client, company)
|
||||
except ROA2WebError as e:
|
||||
return f"Eroare roa2web: {e}"
|
||||
period = data.get("accounting_period") or {}
|
||||
luna, an = period.get("luna"), period.get("an")
|
||||
period_label = f"{_RO_MONTHS[luna]} {an}" if luna and an else ""
|
||||
header = _header(name, period_label)
|
||||
invoices = data.get("invoices", [])
|
||||
total_count = data.get("total_count", len(invoices))
|
||||
total_sold = data.get("total_sold_all", sum(float(i.get("soldfinal", 0)) for i in invoices))
|
||||
summary_line = f"Facturi neîncasate: {total_count}, sold total ~{_fmt_lei(total_sold)} lei"
|
||||
if not invoices:
|
||||
return f"{header}\n{summary_line}"
|
||||
top = sorted(invoices, key=lambda i: float(i.get("soldfinal", 0)), reverse=True)[:8]
|
||||
lines = [
|
||||
f" #{i['nract']} {i['nume']} — ~{_fmt_lei(i['soldfinal'])} lei (scad. {i['datascad']})"
|
||||
for i in top
|
||||
]
|
||||
return f"{header}\n{summary_line}\nTop:\n" + "\n".join(lines)
|
||||
|
||||
|
||||
def cmd_firme(args: list[str]) -> str:
|
||||
"""Listă firme roa2web, opțional filtrate după nume."""
|
||||
client, resolve_company, ROA2WebError = _roa2web()
|
||||
companies = client.companies()
|
||||
if args:
|
||||
needle = " ".join(args).lower()
|
||||
companies = [c for c in companies if needle in c["name"].lower()]
|
||||
if not companies:
|
||||
return "Nicio firmă găsită."
|
||||
lines = [f"{c['id_firma']}\t{c['name']}" for c in companies[:25]]
|
||||
suffix = f"\n… ({len(companies) - 25} în plus, rafinează căutarea)" if len(companies) > 25 else ""
|
||||
return "\n".join(lines) + suffix
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Vremea (Open-Meteo — fără API key)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_WMO_CODES = {
|
||||
0: "cer senin", 1: "cer parțial senin", 2: "înnorat parțial", 3: "cer acoperit",
|
||||
45: "ceață", 48: "ceață cu chiciură",
|
||||
51: "burniță slabă", 53: "burniță", 55: "burniță intensă",
|
||||
56: "burniță înghețată", 57: "burniță înghețată intensă",
|
||||
61: "ploaie slabă", 63: "ploaie", 65: "ploaie puternică",
|
||||
66: "ploaie înghețată", 67: "ploaie înghețată intensă",
|
||||
71: "ninsoare slabă", 73: "ninsoare", 75: "ninsoare puternică", 77: "lapoviță",
|
||||
80: "averse slabe", 81: "averse", 82: "averse puternice",
|
||||
85: "averse de ninsoare", 86: "averse de ninsoare puternice",
|
||||
95: "furtună", 96: "furtună cu grindină", 99: "furtună cu grindină puternică",
|
||||
}
|
||||
|
||||
_DEFAULT_CITY = "Constanța"
|
||||
|
||||
|
||||
def cmd_vremea(args: list[str]) -> str:
|
||||
"""Vremea curentă (Open-Meteo). Args: [oraș] (implicit Constanța)."""
|
||||
city = " ".join(args).strip() if args else _DEFAULT_CITY
|
||||
try:
|
||||
geo = requests.get(
|
||||
"https://geocoding-api.open-meteo.com/v1/search",
|
||||
params={"name": city, "count": 1, "language": "ro", "country": "RO"},
|
||||
timeout=10,
|
||||
).json()
|
||||
results = geo.get("results")
|
||||
if not results:
|
||||
return f"Nu am găsit orașul '{city}'."
|
||||
loc = results[0]
|
||||
name = loc.get("name", city)
|
||||
|
||||
wx = requests.get(
|
||||
"https://api.open-meteo.com/v1/forecast",
|
||||
params={
|
||||
"latitude": loc["latitude"],
|
||||
"longitude": loc["longitude"],
|
||||
"current": "temperature_2m,weather_code,wind_speed_10m",
|
||||
"timezone": "Europe/Bucharest",
|
||||
},
|
||||
timeout=10,
|
||||
).json()
|
||||
cur = wx["current"]
|
||||
except Exception as e:
|
||||
return f"Eroare vremea: {e}"
|
||||
|
||||
temp = round(cur["temperature_2m"])
|
||||
wind = round(cur["wind_speed_10m"])
|
||||
cond = _WMO_CODES.get(cur["weather_code"], "")
|
||||
return f"{name}: {temp}°C, {cond}, vânt {wind} km/h"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Ops & Diagnostics
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -688,6 +863,15 @@ Reminders:
|
||||
/remind <HH:MM> <text> — Reminder today
|
||||
/remind <YYYY-MM-DD> <HH:MM> <text> — Reminder on date
|
||||
|
||||
Financiar (roa2web):
|
||||
/sold [firmă] — Sold casă + bancă
|
||||
/trezorerie [firmă] — Detaliu pe conturi
|
||||
/facturi [firmă] — Facturi neîncasate (top sold)
|
||||
/firme [nume] — Listă firme roa2web
|
||||
|
||||
Vremea:
|
||||
/vremea [oraș] — Vremea curentă (implicit Constanța)
|
||||
|
||||
Audio:
|
||||
/audio <text> — TTS pe text
|
||||
/audio <url> — Extrage articol → audio
|
||||
@@ -924,6 +1108,17 @@ def _claude_summarize(text: str) -> str | None:
|
||||
return None
|
||||
|
||||
|
||||
def cmd_testfallback(args: list[str]) -> str:
|
||||
"""Testează fallback-ul local (Qwen3.5-2B pe LXC 104) fără să fie nevoie de un rate-limit real la Claude."""
|
||||
from src.router import _local_fallback_reply # local import: evită circular import cu router.py
|
||||
|
||||
text = " ".join(args) if args else "Salut! Ce mai faci?"
|
||||
reply = _local_fallback_reply(text)
|
||||
if reply is None:
|
||||
return "Fallback local indisponibil — verifică serviciul llama-qwen35 pe LXC 104 (10.0.20.161:8091)."
|
||||
return reply
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Dispatch
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -940,11 +1135,17 @@ COMMANDS: dict[str, Callable] = {
|
||||
"search": cmd_search,
|
||||
"kb": cmd_kb,
|
||||
"remind": cmd_remind,
|
||||
"sold": cmd_sold,
|
||||
"trezorerie": cmd_trezorerie,
|
||||
"facturi": cmd_facturi,
|
||||
"firme": cmd_firme,
|
||||
"vremea": cmd_vremea,
|
||||
"logs": cmd_logs,
|
||||
"doctor": cmd_doctor,
|
||||
"heartbeat": cmd_heartbeat,
|
||||
"help": cmd_help,
|
||||
"audio": cmd_audio,
|
||||
"testfallback": cmd_testfallback,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Callable
|
||||
|
||||
import requests
|
||||
|
||||
from src.config import Config
|
||||
from src.fast_commands import dispatch as fast_dispatch, set_channel_context
|
||||
from src.last_response_store import set_last as _set_last_response
|
||||
@@ -87,6 +89,63 @@ def _get_config() -> Config:
|
||||
return _config
|
||||
|
||||
|
||||
# Claude CLI rate-limit errors look like:
|
||||
# "Claude CLI error (exit 1): You've hit your session limit · resets 10:50am (UTC)"
|
||||
# (also seen for the total subscription limit, not just per-session — same phrasing).
|
||||
_RATE_LIMIT_RE = re.compile(r"hit your .*limit", re.IGNORECASE)
|
||||
|
||||
_LOCAL_FALLBACK_SYSTEM_PROMPT = (
|
||||
"Ești Echo, asistentul personal al lui Marius, dar rulezi temporar pe un "
|
||||
"model local mic pentru că Claude a atins limita de rate. Nu ai acces la "
|
||||
"unelte, memorie sau istoricul conversației — răspunde scurt și direct, "
|
||||
"doar la mesajul curent, în limba în care a fost scris."
|
||||
)
|
||||
|
||||
_LOCAL_FALLBACK_PREFIX = (
|
||||
"⚠️ Claude e la limită — răspund temporar pe un model local, mai simplu "
|
||||
"(fără istoric, fără unelte):\n\n"
|
||||
)
|
||||
|
||||
|
||||
def _is_rate_limit_error(err: Exception) -> bool:
|
||||
return bool(_RATE_LIMIT_RE.search(str(err)))
|
||||
|
||||
|
||||
def _local_fallback_reply(text: str) -> str | None:
|
||||
"""Best-effort reply from the local llama.cpp fallback (LXC 104, Qwen3.5-2B).
|
||||
|
||||
Returns None if the fallback itself is unreachable/fails, so the caller
|
||||
can fall back further to surfacing the original Claude error.
|
||||
"""
|
||||
cfg = _get_config().get("local_fallback", {}) or {}
|
||||
if not cfg.get("enabled", False):
|
||||
return None
|
||||
url = cfg.get("url")
|
||||
if not url:
|
||||
return None
|
||||
try:
|
||||
resp = requests.post(
|
||||
url,
|
||||
json={
|
||||
"messages": [
|
||||
{"role": "system", "content": _LOCAL_FALLBACK_SYSTEM_PROMPT},
|
||||
{"role": "user", "content": text},
|
||||
],
|
||||
"temperature": 0.3,
|
||||
"max_tokens": 500,
|
||||
},
|
||||
timeout=45,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
content = resp.json()["choices"][0]["message"]["content"].strip()
|
||||
if not content:
|
||||
return None
|
||||
return _LOCAL_FALLBACK_PREFIX + content
|
||||
except Exception as e: # noqa: BLE001
|
||||
log.error("Local fallback LLM failed: %s", e)
|
||||
return None
|
||||
|
||||
|
||||
def route_message(
|
||||
channel_id: str,
|
||||
user_id: str,
|
||||
@@ -218,6 +277,12 @@ def route_message(
|
||||
return response, False
|
||||
except Exception as e:
|
||||
log.error("Claude error for channel %s: %s", channel_id, e)
|
||||
if _is_rate_limit_error(e):
|
||||
log.warning("Rate limit detected for channel %s — trying local fallback", channel_id)
|
||||
fallback = _local_fallback_reply(text)
|
||||
if fallback is not None:
|
||||
_set_last_response(channel_id, fallback)
|
||||
return fallback, False
|
||||
return f"Error: {e}", False
|
||||
|
||||
|
||||
|
||||
@@ -418,9 +418,15 @@ class Scheduler:
|
||||
)
|
||||
|
||||
if proc.returncode != 0:
|
||||
error_msg = proc.stderr[:500] if proc.stderr else "unknown error"
|
||||
error_msg = proc.stderr[:500].strip() if proc.stderr else ""
|
||||
if not error_msg and proc.stdout:
|
||||
try:
|
||||
error_msg = json.loads(proc.stdout).get("result", "")
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
error_msg = (error_msg or proc.stdout)[:500].strip()
|
||||
raise RuntimeError(
|
||||
f"Claude CLI error (exit {proc.returncode}): {error_msg}"
|
||||
f"Claude CLI error (exit {proc.returncode}): {error_msg or 'unknown error'}"
|
||||
)
|
||||
|
||||
data = json.loads(proc.stdout)
|
||||
|
||||
@@ -94,6 +94,31 @@ def expand_numbers_ro(text: str) -> str:
|
||||
return _NUM_TOKEN.sub(_sub, text)
|
||||
|
||||
|
||||
def _decimal_to_en(s: str) -> str:
|
||||
"""Convert decimal string 'X.Y' to English words ('3.14' -> 'three point one four')."""
|
||||
int_part, dec_part = s.split('.', 1)
|
||||
int_words = num2words(int(int_part), lang='en')
|
||||
dec_words = ' '.join(num2words(int(d), lang='en') for d in dec_part)
|
||||
return f"{int_words} point {dec_words}"
|
||||
|
||||
|
||||
def expand_numbers_en(text: str) -> str:
|
||||
"""Expand bare numeric tokens to English words.
|
||||
|
||||
Mirrors expand_numbers_ro for the pocket-tts (English-only) path.
|
||||
Must run after normalize_thousands so Romanian-style grouped
|
||||
integers ("105.300") read as one magnitude ("one hundred five
|
||||
thousand three hundred") instead of a decimal ("105.3").
|
||||
"""
|
||||
def _sub(match: re.Match) -> str:
|
||||
token = match.group(1)
|
||||
if '.' in token:
|
||||
return _decimal_to_en(token)
|
||||
return num2words(int(token), lang='en')
|
||||
|
||||
return _NUM_TOKEN.sub(_sub, text)
|
||||
|
||||
|
||||
# ---------- Thousands separator ----------
|
||||
|
||||
# Romanian uses dot or space as thousands separator: 384.000 / 384 000. The
|
||||
@@ -268,8 +293,8 @@ def expand_currency(text: str) -> str:
|
||||
# ---------- Symbols ----------
|
||||
|
||||
_SYMBOL_WORDS = {
|
||||
'ro': {'%': ' la sută', '&': ' și ', '@': ' la ', '°': ' grade'},
|
||||
'en': {'%': ' percent', '&': ' and ', '@': ' at ', '°': ' degrees'},
|
||||
'ro': {'%': ' la sută', '&': ' și ', '@': ' la ', '°': ' grade', '~': ' aproximativ '},
|
||||
'en': {'%': ' percent', '&': ' and ', '@': ' at ', '°': ' degrees', '~': ' about '},
|
||||
}
|
||||
|
||||
|
||||
@@ -322,11 +347,14 @@ def expand_for_tts(text: str, lang: str = 'ro') -> str:
|
||||
"Restul l-am scris în chat." suffix from normalize_for_tts() would be
|
||||
misleading (no live chat mirror exists for that flow).
|
||||
|
||||
The RO-specific expansions (abbreviations, thousands, time, currency,
|
||||
units, numbers-to-words) only make sense for Romanian text — for other
|
||||
languages (e.g. English text routed to pocket-tts, which is
|
||||
English-only) they'd inject Romanian words/diacritics into text the
|
||||
target engine can't speak. Skip them when lang != 'ro'.
|
||||
The RO-specific expansions (abbreviations, time, currency wording,
|
||||
units) only make sense for Romanian text — for other languages (e.g.
|
||||
English text routed to pocket-tts, which is English-only) they'd
|
||||
inject Romanian words/diacritics into text the target engine can't
|
||||
speak, so they're skipped when lang != 'ro'. Thousands-grouping and
|
||||
numbers-to-words still run for lang == 'en', with English wording,
|
||||
since replies on English turns often carry Romanian-formatted figures
|
||||
("105.300 lei") that pocket-tts would otherwise misread as decimals.
|
||||
"""
|
||||
text = strip_markdown(text)
|
||||
text = sanitize_punctuation(text)
|
||||
@@ -337,6 +365,16 @@ def expand_for_tts(text: str, lang: str = 'ro') -> str:
|
||||
text = expand_currency(text)
|
||||
text = expand_units(text)
|
||||
text = expand_numbers_ro(text)
|
||||
elif lang == 'en':
|
||||
# RO-specific expansions (abbreviations, time, currency wording,
|
||||
# units) are skipped here — see docstring above. But bare numbers
|
||||
# still need expanding: replies to Marius often carry Romanian
|
||||
# thousands-grouped figures ("105.300 lei") even on English turns,
|
||||
# and pocket-tts reads the dot as an English decimal point if left
|
||||
# alone ("one hundred five point three" instead of "one hundred
|
||||
# five thousand three hundred").
|
||||
text = normalize_thousands(text)
|
||||
text = expand_numbers_en(text)
|
||||
text = expand_symbols(text, lang=lang)
|
||||
return text.strip()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user