Sagar Patel commited on
Commit
fb99e77
·
1 Parent(s): 2808a57

Add language polish and seller shortcuts

Browse files
README.md CHANGED
@@ -19,13 +19,16 @@ The submission version is feature-frozen around the complete demo loop:
19
 
20
  - Record a transaction with your microphone and transcribe it with faster-whisper.
21
  - Configure seller context: business name, currency label, low-stock threshold, and language style.
 
22
  - Use a first-run “Start in 60 seconds” guide for setup, first transaction, review, and closeout.
23
  - Type or paste a transaction note.
24
  - Bulk import multiple pasted notes for review and editing.
25
  - Parse it with Modal-hosted NVIDIA Nemotron when configured, with local rules as a deterministic fallback.
26
  - Parse common English, Hinglish, Hindi-lite, Gujarati-lite, Spanish, French, and Portuguese seller phrases with deterministic fallback rules.
27
  - Review a human-friendly transaction card with warning badges before saving.
 
28
  - Correct transaction type, item, customer, quantity, price, amount, notes, and confidence directly before saving.
 
29
  - Choose `Cloud AI first` or `Local fallback only` from the Record screen to make the AI route explicit during demos.
30
  - Save the structured transaction to SQLite.
31
  - See a “Saved just now” receipt with bookkeeping side effects.
@@ -34,7 +37,8 @@ The submission version is feature-frozen around the complete demo loop:
34
  - Use a mobile-first, business-style Gradio interface with custom styling.
35
  - Run a Daily Closeout that prepares PDF, WhatsApp summary, and CSV exports together.
36
  - Download a Daily Summary PDF report.
37
- - Generate a WhatsApp-ready daily business summary.
 
38
  - Generate customer follow-up reminders and an inventory reorder list.
39
  - Capture field-test evidence with a seller checklist and anonymized feedback notes.
40
  - Offload speech transcription and LLM parsing to optional Modal endpoints.
@@ -106,7 +110,7 @@ The same examples are available in `sample_data/demo_transactions.txt`.
106
  2. Use the `Hackathon Demo Launchpad` on the first screen to seed demo transactions.
107
  3. Open `Record Text & Voice`, speak or type `Sold 12 mangoes, 20 each`, parse it, and review the human-readable transaction card.
108
  4. Correct one inline review field before saving, such as quantity or amount, to show the seller correction loop.
109
- 5. Show the parse status and warning badges: Modal/NVIDIA Nemotron when available, local fallback when needed.
110
  6. Save the transaction and show the Command Center plus the receipt with stock, customer, or amount side effects.
111
  7. Open `Dashboard`, `Customer Credit`, and `Inventory` to show the Insight Coach, timeline charts, customer detail, follow-up message, inventory detail, reorder list, and automatic bookkeeping updates.
112
  8. Open `Field Test` to show what a seller tried and what changed after feedback.
@@ -196,10 +200,14 @@ curl -X POST https://sagarpat3199--voiceledger-api.modal.run/parse \
196
  - Smart review warnings flag low confidence, missing fields, duplicate risk, and negative stock before save.
197
  - Inline review editing lets sellers correct the parsed transaction before the save touches the ledger.
198
  - Seller setup persists business name, currency label, low-stock threshold, and language style in SQLite.
 
199
  - Field Test persists anonymized seller evidence and a workflow checklist in SQLite.
 
200
  - Local fallback rules include common English, Hinglish, Hindi-lite, Gujarati-lite, Spanish, French, and Portuguese seller notes.
 
201
  - Customer follow-up and inventory reorder helpers generate WhatsApp-ready action messages.
202
- - WhatsApp summaries provide a short copyable daily recap for sharing.
 
203
  - Bulk import splits pasted notes by line, parses each line, supports review edits, and saves all reviewed transactions.
204
  - Modal integration lives in `backend/`; if endpoint URLs are not configured, local fallback stays active.
205
  - The Record screen includes a local-only AI mode that skips Modal while preserving the same transaction review and save flow.
 
19
 
20
  - Record a transaction with your microphone and transcribe it with faster-whisper.
21
  - Configure seller context: business name, currency label, low-stock threshold, and language style.
22
+ - Pick common currency presets for INR, USD, EUR, GBP, MXN, and BRL, or enter a custom currency label.
23
  - Use a first-run “Start in 60 seconds” guide for setup, first transaction, review, and closeout.
24
  - Type or paste a transaction note.
25
  - Bulk import multiple pasted notes for review and editing.
26
  - Parse it with Modal-hosted NVIDIA Nemotron when configured, with local rules as a deterministic fallback.
27
  - Parse common English, Hinglish, Hindi-lite, Gujarati-lite, Spanish, French, and Portuguese seller phrases with deterministic fallback rules.
28
  - Review a human-friendly transaction card with warning badges before saving.
29
+ - See a language/confidence chip after parsing so multilingual notes are visibly handled.
30
  - Correct transaction type, item, customer, quantity, price, amount, notes, and confidence directly before saving.
31
+ - Keep a correction log for review edits so model/rule mistakes become visible field-test evidence.
32
  - Choose `Cloud AI first` or `Local fallback only` from the Record screen to make the AI route explicit during demos.
33
  - Save the structured transaction to SQLite.
34
  - See a “Saved just now” receipt with bookkeeping side effects.
 
37
  - Use a mobile-first, business-style Gradio interface with custom styling.
38
  - Run a Daily Closeout that prepares PDF, WhatsApp summary, and CSV exports together.
39
  - Download a Daily Summary PDF report.
40
+ - Generate WhatsApp-ready daily business summaries in English, Spanish, French, or Portuguese.
41
+ - Run shortcut commands like `close today`, `show Amit`, or `stock mangoes`.
42
  - Generate customer follow-up reminders and an inventory reorder list.
43
  - Capture field-test evidence with a seller checklist and anonymized feedback notes.
44
  - Offload speech transcription and LLM parsing to optional Modal endpoints.
 
110
  2. Use the `Hackathon Demo Launchpad` on the first screen to seed demo transactions.
111
  3. Open `Record Text & Voice`, speak or type `Sold 12 mangoes, 20 each`, parse it, and review the human-readable transaction card.
112
  4. Correct one inline review field before saving, such as quantity or amount, to show the seller correction loop.
113
+ 5. Show the parse status, language/confidence chip, and warning badges: Modal/NVIDIA Nemotron when available, local fallback when needed.
114
  6. Save the transaction and show the Command Center plus the receipt with stock, customer, or amount side effects.
115
  7. Open `Dashboard`, `Customer Credit`, and `Inventory` to show the Insight Coach, timeline charts, customer detail, follow-up message, inventory detail, reorder list, and automatic bookkeeping updates.
116
  8. Open `Field Test` to show what a seller tried and what changed after feedback.
 
200
  - Smart review warnings flag low confidence, missing fields, duplicate risk, and negative stock before save.
201
  - Inline review editing lets sellers correct the parsed transaction before the save touches the ledger.
202
  - Seller setup persists business name, currency label, low-stock threshold, and language style in SQLite.
203
+ - Currency presets make the app easier to demo for INR, USD, EUR, GBP, MXN, BRL, and custom local labels.
204
  - Field Test persists anonymized seller evidence and a workflow checklist in SQLite.
205
+ - The Field Test mistake log records corrected fields before save so product feedback is visible.
206
  - Local fallback rules include common English, Hinglish, Hindi-lite, Gujarati-lite, Spanish, French, and Portuguese seller notes.
207
+ - Parse status includes a lightweight language/confidence chip for multilingual seller notes.
208
  - Customer follow-up and inventory reorder helpers generate WhatsApp-ready action messages.
209
+ - WhatsApp summaries provide short copyable daily recaps in English, Spanish, French, or Portuguese.
210
+ - Voice command shortcuts provide quick access to daily closeout, customer detail, and inventory detail.
211
  - Bulk import splits pasted notes by line, parses each line, supports review edits, and saves all reviewed transactions.
212
  - Modal integration lives in `backend/`; if endpoint URLs are not configured, local fallback stays active.
213
  - The Record screen includes a local-only AI mode that skips Modal while preserving the same transaction review and save flow.
docs/demo-video-script.md CHANGED
@@ -16,7 +16,7 @@ On the first screen, show the `Judge Demo Flow` panel:
16
 
17
  Click `Seed Demo Transactions`.
18
 
19
- Show `Seller Setup` and mention that business name, currency, low-stock threshold, and language style are saved in SQLite.
20
 
21
  Show the first-run guide and multilingual examples so judges see English, Hinglish, Gujarati-lite, Spanish, French, and Portuguese seller notes.
22
 
@@ -50,7 +50,9 @@ Click parse. Show:
50
  - Inline review fields for correcting item, quantity, amount, customer, and notes before saving
51
  - Warning badges for low confidence, missing fields, duplicate risk, or negative stock when present
52
  - Parse source/status
 
53
  - Command Center update
 
54
  - “Saved just now” receipt with stock, customer, or amount side effects
55
 
56
  ## 0:45-1:05 — Bookkeeping Updates
@@ -71,13 +73,13 @@ In `Ledger`, load a transaction by id, update or delete it, and show balances re
71
 
72
  Click `Download CSV`.
73
 
74
- Open `Reports & PDF`, run `Daily Closeout`, then generate the PDF, WhatsApp summary, and CSV export.
75
 
76
  Suggested screenshot/GIF moments:
77
 
78
  - Record flow: first-run guide, multilingual examples, review card, inline correction, parse source, warning badges, save receipt.
79
  - Seller setup: currency/threshold/language style and Command Center.
80
- - Field Test: seller checklist and feedback notes.
81
  - Submission Story: AI pipeline and small-model fit card.
82
  - Dashboard: sales, expenses, profit, credit, timeline, top item, low-stock inventory.
83
  - Reports/Ledger: Daily Closeout, PDF download, WhatsApp summary, CSV export.
 
16
 
17
  Click `Seed Demo Transactions`.
18
 
19
+ Show `Seller Setup` and mention that business name, currency preset, low-stock threshold, and language style are saved in SQLite.
20
 
21
  Show the first-run guide and multilingual examples so judges see English, Hinglish, Gujarati-lite, Spanish, French, and Portuguese seller notes.
22
 
 
50
  - Inline review fields for correcting item, quantity, amount, customer, and notes before saving
51
  - Warning badges for low confidence, missing fields, duplicate risk, or negative stock when present
52
  - Parse source/status
53
+ - Language/confidence chip for multilingual notes
54
  - Command Center update
55
+ - Voice command shortcuts such as `close today`, `show Amit`, and `stock mangoes`
56
  - “Saved just now” receipt with stock, customer, or amount side effects
57
 
58
  ## 0:45-1:05 — Bookkeeping Updates
 
73
 
74
  Click `Download CSV`.
75
 
76
+ Open `Reports & PDF`, run `Daily Closeout`, then generate the PDF, translated WhatsApp summary, and CSV export.
77
 
78
  Suggested screenshot/GIF moments:
79
 
80
  - Record flow: first-run guide, multilingual examples, review card, inline correction, parse source, warning badges, save receipt.
81
  - Seller setup: currency/threshold/language style and Command Center.
82
+ - Field Test: seller checklist, feedback notes, and correction log.
83
  - Submission Story: AI pipeline and small-model fit card.
84
  - Dashboard: sales, expenses, profit, credit, timeline, top item, low-stock inventory.
85
  - Reports/Ledger: Daily Closeout, PDF download, WhatsApp summary, CSV export.
docs/field-notes.md CHANGED
@@ -50,8 +50,9 @@ The hackathon version is intentionally frozen around the complete bookkeeping lo
50
  - Voice and text capture.
51
  - Modal/Nemotron parsing with local rule fallback.
52
  - Seller setup for business name, currency, low-stock threshold, and language style.
 
53
  - Hinglish/Hindi-lite/Gujarati-lite/Spanish/French/Portuguese fallback examples for common seller notes.
54
- - First-run onboarding, multilingual examples, human-friendly review cards, inline review correction, warning badges, receipts, and save feedback.
55
  - Command Center, dashboard timeline, seller-day timeline, customer follow-up, inventory reorder list, ledger correction, Daily Closeout, PDF, CSV, and WhatsApp exports.
56
  - Field Test Mode for the seller checklist and anonymized “who/tried/changed” feedback evidence.
57
  - Demo Health for backend observability.
@@ -61,13 +62,13 @@ This freeze keeps the demo reliable and makes the value proposition easier to ju
61
 
62
  ## What We Learned
63
 
64
- The strongest product behavior is not the model call itself. It is the reliable loop around it: voice input, readable review, warning badges, save receipt, correction, and export. Informal sellers need speed, but they also need a way to spot mistakes and fix them. Edit/delete, detail drilldowns, Daily Closeout, and CSV export make the app credible as a bookkeeping tool instead of a parser demo.
65
 
66
  The small-model constraint helped keep the app honest. The model handles the fuzzy human input; the code owns the accounting state.
67
 
68
  ## Screenshot Moments
69
 
70
- - Record flow: first-run onboarding, multilingual examples, review card, inline corrections, warning badges, source/status, save receipt.
71
  - Dashboard: daily totals, Insight Coach, outstanding credit, sales/expense timeline, top seller, low-stock inventory.
72
- - Field Test: workflow checklist and anonymized feedback evidence.
73
  - Reports and exports: Daily Closeout, PDF download, WhatsApp summary, CSV ledger export.
 
50
  - Voice and text capture.
51
  - Modal/Nemotron parsing with local rule fallback.
52
  - Seller setup for business name, currency, low-stock threshold, and language style.
53
+ - Currency presets and translated WhatsApp summaries for sellers who demo in INR, USD, EUR, GBP, MXN, BRL, English, Spanish, French, or Portuguese contexts.
54
  - Hinglish/Hindi-lite/Gujarati-lite/Spanish/French/Portuguese fallback examples for common seller notes.
55
+ - First-run onboarding, multilingual examples, language/confidence chips, human-friendly review cards, inline review correction, mistake logging, warning badges, receipts, and save feedback.
56
  - Command Center, dashboard timeline, seller-day timeline, customer follow-up, inventory reorder list, ledger correction, Daily Closeout, PDF, CSV, and WhatsApp exports.
57
  - Field Test Mode for the seller checklist and anonymized “who/tried/changed” feedback evidence.
58
  - Demo Health for backend observability.
 
62
 
63
  ## What We Learned
64
 
65
+ The strongest product behavior is not the model call itself. It is the reliable loop around it: voice input, readable review, warning badges, save receipt, correction, and export. Informal sellers need speed, but they also need a way to spot mistakes and fix them. The correction log makes those mistakes measurable during field tests, while edit/delete, detail drilldowns, Daily Closeout, and CSV export make the app credible as a bookkeeping tool instead of a parser demo.
66
 
67
  The small-model constraint helped keep the app honest. The model handles the fuzzy human input; the code owns the accounting state.
68
 
69
  ## Screenshot Moments
70
 
71
+ - Record flow: first-run onboarding, multilingual examples, language/confidence chip, voice command shortcuts, review card, inline corrections, warning badges, source/status, save receipt.
72
  - Dashboard: daily totals, Insight Coach, outstanding credit, sales/expense timeline, top seller, low-stock inventory.
73
+ - Field Test: workflow checklist, correction log, and anonymized feedback evidence.
74
  - Reports and exports: Daily Closeout, PDF download, WhatsApp summary, CSV ledger export.
docs/submission-checklist.md CHANGED
@@ -17,13 +17,16 @@
17
  - `Demo Health` shows Modal reachable and backend version.
18
  - `Demo Health` shows NVIDIA Nemotron parser status.
19
  - Seller Setup saves business name, currency label, low-stock threshold, and language style.
 
20
  - Text parse works with `Sold 12 mangoes, 20 each`.
21
  - Local fallback parses `Amit ne 100 dene hai`, `Amit ne 50 diya`, and `50 mango kharida`.
22
  - Voice parse works and transcript appears.
23
  - Review card shows type, item, quantity, price, amount, customer, source, and confidence.
 
24
  - Smart warnings appear for low confidence, missing fields, duplicate risk, and negative stock.
25
  - Save shows a receipt with transaction type, amount, and side effects.
26
  - Command Center updates after save and seed.
 
27
  - Save updates Dashboard and Ledger.
28
  - Dashboard timeline loads from saved transactions.
29
  - Seller-day timeline shows recent saved transactions.
@@ -37,7 +40,8 @@
37
  - CSV export downloads from Ledger.
38
  - Daily Closeout generates PDF, CSV, WhatsApp summary, and a status line.
39
  - PDF report downloads from Reports.
40
- - WhatsApp summary generates copyable text.
 
41
 
42
  ## Prize Alignment
43
 
 
17
  - `Demo Health` shows Modal reachable and backend version.
18
  - `Demo Health` shows NVIDIA Nemotron parser status.
19
  - Seller Setup saves business name, currency label, low-stock threshold, and language style.
20
+ - Currency preset updates the currency label for INR, USD, EUR, GBP, MXN, and BRL.
21
  - Text parse works with `Sold 12 mangoes, 20 each`.
22
  - Local fallback parses `Amit ne 100 dene hai`, `Amit ne 50 diya`, and `50 mango kharida`.
23
  - Voice parse works and transcript appears.
24
  - Review card shows type, item, quantity, price, amount, customer, source, and confidence.
25
+ - Parse status shows source plus language/confidence.
26
  - Smart warnings appear for low confidence, missing fields, duplicate risk, and negative stock.
27
  - Save shows a receipt with transaction type, amount, and side effects.
28
  - Command Center updates after save and seed.
29
+ - Voice command shortcuts work for `close today`, `show Amit`, and `stock mangoes`.
30
  - Save updates Dashboard and Ledger.
31
  - Dashboard timeline loads from saved transactions.
32
  - Seller-day timeline shows recent saved transactions.
 
40
  - CSV export downloads from Ledger.
41
  - Daily Closeout generates PDF, CSV, WhatsApp summary, and a status line.
42
  - PDF report downloads from Reports.
43
+ - WhatsApp summary generates copyable text in English, Spanish, French, and Portuguese.
44
+ - Field Test correction log shows edited review fields.
45
 
46
  ## Prize Alignment
47
 
tests/test_ui_audio_flow.py CHANGED
@@ -97,6 +97,23 @@ def test_parse_note_local_mode_surfaces_local_fallback(monkeypatch) -> None:
97
  assert "Safe to save" in review_card
98
 
99
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  def test_high_contrast_demo_panels_are_rendered() -> None:
101
  panel = gradio_app._info_panel(
102
  "Demo Health",
@@ -210,6 +227,9 @@ def test_apply_review_edits_updates_payload_and_review_card(tmp_path) -> None:
210
  assert state == payload
211
  assert "Review updated" in status
212
  assert "250" in review_card
 
 
 
213
 
214
 
215
  def test_receipt_card_summarizes_saved_sale(tmp_path) -> None:
@@ -267,6 +287,18 @@ def test_customer_followup_and_reorder_helpers(tmp_path) -> None:
267
  assert "Onions" in message
268
 
269
 
 
 
 
 
 
 
 
 
 
 
 
 
270
  def test_insight_coach_surfaces_credit_and_stock_actions(tmp_path) -> None:
271
  db_path = tmp_path / "voiceledger.sqlite3"
272
  gradio_app.add_transaction(gradio_app.local_parse_transaction("Amit owes 100"), db_path)
 
97
  assert "Safe to save" in review_card
98
 
99
 
100
+ def test_parse_note_surfaces_language_confidence_chip(monkeypatch) -> None:
101
+ monkeypatch.setattr(
102
+ gradio_app.modal_api,
103
+ "parse_transaction_result",
104
+ lambda text, fallback, **kwargs: ParseResult(
105
+ transaction=fallback(text),
106
+ source="local",
107
+ message="Parsed locally with the rule parser.",
108
+ ),
109
+ )
110
+
111
+ _, _, status, _ = gradio_app._parse_note("Vendí 12 mangos, 20 cada uno")
112
+
113
+ assert "Spanish" in status
114
+ assert "High confidence" in status
115
+
116
+
117
  def test_high_contrast_demo_panels_are_rendered() -> None:
118
  panel = gradio_app._info_panel(
119
  "Demo Health",
 
227
  assert state == payload
228
  assert "Review updated" in status
229
  assert "250" in review_card
230
+ correction_log = gradio_app.get_correction_log(db_path)
231
+ assert len(correction_log) == 1
232
+ assert "quantity" in correction_log.iloc[0]["changed_fields"]
233
 
234
 
235
  def test_receipt_card_summarizes_saved_sale(tmp_path) -> None:
 
287
  assert "Onions" in message
288
 
289
 
290
+ def test_currency_presets_and_voice_commands(tmp_path) -> None:
291
+ db_path = tmp_path / "voiceledger.sqlite3"
292
+ gradio_app.add_transaction(gradio_app.local_parse_transaction("Amit owes 100"), db_path)
293
+ gradio_app.add_transaction(gradio_app.local_parse_transaction("Bought 3 onions"), db_path)
294
+
295
+ assert gradio_app._currency_symbol_for_preset("Brazil - BRL (R$)") == "R$"
296
+ assert gradio_app._currency_preset_for_symbol("€") == "European Union - EUR (€)"
297
+ assert "Amit" in gradio_app._run_voice_command("show Amit", db_path)
298
+ assert "onions" in gradio_app._run_voice_command("stock onions", db_path)
299
+ assert "Daily Closeout Ready" in gradio_app._run_voice_command("close today", db_path)
300
+
301
+
302
  def test_insight_coach_surfaces_credit_and_stock_actions(tmp_path) -> None:
303
  db_path = tmp_path / "voiceledger.sqlite3"
304
  gradio_app.add_transaction(gradio_app.local_parse_transaction("Amit owes 100"), db_path)
tests/test_whatsapp_summary.py CHANGED
@@ -35,3 +35,19 @@ def test_generate_whatsapp_summary_handles_empty_data(tmp_path: Path) -> None:
35
  assert "Outstanding Credit: ₹0" in summary
36
  assert "Top Product: None" in summary
37
  assert "Low Stock: None" in summary
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  assert "Outstanding Credit: ₹0" in summary
36
  assert "Top Product: None" in summary
37
  assert "Low Stock: None" in summary
38
+
39
+
40
+ def test_generate_whatsapp_summary_supports_language_labels(tmp_path: Path) -> None:
41
+ db_path = tmp_path / "voiceledger.sqlite3"
42
+ add_transaction(parse_transaction("Vendí 12 mangos, 20 cada uno"), db_path)
43
+
44
+ spanish = generate_whatsapp_summary(db_path=db_path, language="Spanish", currency_symbol="$")
45
+ french = generate_whatsapp_summary(db_path=db_path, language="French", currency_symbol="€")
46
+ portuguese = generate_whatsapp_summary(db_path=db_path, language="Portuguese", currency_symbol="R$")
47
+
48
+ assert "Resumen Diario" in spanish
49
+ assert "Ventas: $240" in spanish
50
+ assert "Resume Quotidien" in french
51
+ assert "Ventes: €240" in french
52
+ assert "Resumo Diario" in portuguese
53
+ assert "Vendas: R$240" in portuguese
voiceledger/ledger/corrections.py ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Correction log persistence for parsed transaction review edits."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import sqlite3
7
+ from datetime import datetime
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ import pandas as pd
12
+
13
+ from voiceledger.config import get_database_path
14
+
15
+
16
+ CORRECTION_SCHEMA_SQL = """
17
+ CREATE TABLE IF NOT EXISTS correction_log (
18
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
19
+ original_payload TEXT NOT NULL,
20
+ corrected_payload TEXT NOT NULL,
21
+ changed_fields TEXT NOT NULL,
22
+ created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
23
+ );
24
+ """
25
+
26
+ CORRECTION_COLUMNS = ["id", "changed_fields", "original_payload", "corrected_payload", "created_at"]
27
+
28
+
29
+ def initialize_correction_log_table(db_path: str | Path | None = None) -> Path:
30
+ """Create the correction log table if needed."""
31
+ path = _resolve_db_path(db_path)
32
+ path.parent.mkdir(parents=True, exist_ok=True)
33
+ with sqlite3.connect(path) as connection:
34
+ connection.execute(CORRECTION_SCHEMA_SQL)
35
+ connection.commit()
36
+ return path
37
+
38
+
39
+ def record_correction(
40
+ original_payload: dict[str, Any],
41
+ corrected_payload: dict[str, Any],
42
+ db_path: str | Path | None = None,
43
+ ) -> int | None:
44
+ """Persist a correction when review edits changed transaction fields."""
45
+ changed_fields = _changed_fields(original_payload, corrected_payload)
46
+ if not changed_fields:
47
+ return None
48
+
49
+ path = initialize_correction_log_table(db_path)
50
+ with sqlite3.connect(path) as connection:
51
+ cursor = connection.execute(
52
+ """
53
+ INSERT INTO correction_log (
54
+ original_payload,
55
+ corrected_payload,
56
+ changed_fields,
57
+ created_at
58
+ )
59
+ VALUES (?, ?, ?, ?)
60
+ """,
61
+ (
62
+ json.dumps(original_payload, sort_keys=True),
63
+ json.dumps(corrected_payload, sort_keys=True),
64
+ ", ".join(changed_fields),
65
+ datetime.now().isoformat(sep=" ", timespec="seconds"),
66
+ ),
67
+ )
68
+ connection.commit()
69
+ return int(cursor.lastrowid)
70
+
71
+
72
+ def get_correction_log(db_path: str | Path | None = None) -> pd.DataFrame:
73
+ """Return saved parse correction rows."""
74
+ path = initialize_correction_log_table(db_path)
75
+ with sqlite3.connect(path) as connection:
76
+ rows = connection.execute(
77
+ """
78
+ SELECT id, changed_fields, original_payload, corrected_payload, created_at
79
+ FROM correction_log
80
+ ORDER BY id DESC
81
+ """
82
+ ).fetchall()
83
+ records = [dict(zip(CORRECTION_COLUMNS, row, strict=True)) for row in rows]
84
+ return pd.DataFrame.from_records(records, columns=CORRECTION_COLUMNS)
85
+
86
+
87
+ def _changed_fields(original_payload: dict[str, Any], corrected_payload: dict[str, Any]) -> list[str]:
88
+ """Return fields whose values changed after review editing."""
89
+ tracked_fields = (
90
+ "transaction_type",
91
+ "item",
92
+ "quantity",
93
+ "unit_price",
94
+ "amount",
95
+ "customer",
96
+ "payment_status",
97
+ "notes",
98
+ "confidence",
99
+ )
100
+ return [field for field in tracked_fields if original_payload.get(field) != corrected_payload.get(field)]
101
+
102
+
103
+ def _resolve_db_path(db_path: str | Path | None) -> Path:
104
+ """Resolve an explicit or configured database path."""
105
+ if db_path is None:
106
+ return get_database_path()
107
+ return Path(db_path).expanduser()
voiceledger/ledger/database.py CHANGED
@@ -11,6 +11,7 @@ from typing import Any
11
  import pandas as pd
12
 
13
  from voiceledger.config import get_database_path
 
14
  from voiceledger.ledger.customers import add_credit, initialize_customers_table, record_payment
15
  from voiceledger.ledger.inventory import add_stock, initialize_inventory_table, remove_stock
16
  from voiceledger.ledger.settings import initialize_business_settings_table
@@ -60,6 +61,7 @@ def initialize_database(db_path: str | Path | None = None) -> Path:
60
  initialize_customers_table(path)
61
  initialize_inventory_table(path)
62
  initialize_business_settings_table(path)
 
63
  return path
64
 
65
 
 
11
  import pandas as pd
12
 
13
  from voiceledger.config import get_database_path
14
+ from voiceledger.ledger.corrections import initialize_correction_log_table
15
  from voiceledger.ledger.customers import add_credit, initialize_customers_table, record_payment
16
  from voiceledger.ledger.inventory import add_stock, initialize_inventory_table, remove_stock
17
  from voiceledger.ledger.settings import initialize_business_settings_table
 
61
  initialize_customers_table(path)
62
  initialize_inventory_table(path)
63
  initialize_business_settings_table(path)
64
+ initialize_correction_log_table(path)
65
  return path
66
 
67
 
voiceledger/reports/whatsapp_summary.py CHANGED
@@ -23,8 +23,10 @@ def generate_whatsapp_summary(
23
  low_stock_threshold: float = 5,
24
  business_name: str = "VoiceLedger",
25
  currency_symbol: str = "₹",
 
26
  ) -> str:
27
  """Generate a concise daily summary suitable for WhatsApp sharing."""
 
28
  sales = calculate_daily_sales(db_path=db_path, report_date=report_date)
29
  expenses = calculate_daily_expenses(db_path=db_path, report_date=report_date)
30
  profit = calculate_net_profit(db_path=db_path, report_date=report_date)
@@ -34,16 +36,16 @@ def generate_whatsapp_summary(
34
 
35
  return "\n".join(
36
  [
37
- f"{business_name} Daily Summary",
38
  "",
39
- f"Sales: {_format_money(sales, currency_symbol)}",
40
- f"Expenses: {_format_money(expenses, currency_symbol)}",
41
- f"Profit: {_format_money(profit, currency_symbol)}",
42
  "",
43
- f"Outstanding Credit: {_format_money(credit, currency_symbol)}",
44
  "",
45
- f"Top Product: {top_product}",
46
- f"Low Stock: {low_stock}",
47
  ]
48
  )
49
 
@@ -68,3 +70,47 @@ def _format_money(value: float, currency_symbol: str) -> str:
68
  if amount.is_integer():
69
  return f"{currency_symbol}{int(amount)}"
70
  return f"{currency_symbol}{amount:,.2f}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  low_stock_threshold: float = 5,
24
  business_name: str = "VoiceLedger",
25
  currency_symbol: str = "₹",
26
+ language: str = "English",
27
  ) -> str:
28
  """Generate a concise daily summary suitable for WhatsApp sharing."""
29
+ labels = _summary_labels(language)
30
  sales = calculate_daily_sales(db_path=db_path, report_date=report_date)
31
  expenses = calculate_daily_expenses(db_path=db_path, report_date=report_date)
32
  profit = calculate_net_profit(db_path=db_path, report_date=report_date)
 
36
 
37
  return "\n".join(
38
  [
39
+ f"{business_name} {labels['daily_summary']}",
40
  "",
41
+ f"{labels['sales']}: {_format_money(sales, currency_symbol)}",
42
+ f"{labels['expenses']}: {_format_money(expenses, currency_symbol)}",
43
+ f"{labels['profit']}: {_format_money(profit, currency_symbol)}",
44
  "",
45
+ f"{labels['outstanding_credit']}: {_format_money(credit, currency_symbol)}",
46
  "",
47
+ f"{labels['top_product']}: {top_product}",
48
+ f"{labels['low_stock']}: {low_stock}",
49
  ]
50
  )
51
 
 
70
  if amount.is_integer():
71
  return f"{currency_symbol}{int(amount)}"
72
  return f"{currency_symbol}{amount:,.2f}"
73
+
74
+
75
+ def _summary_labels(language: str) -> dict[str, str]:
76
+ """Return localized labels for a WhatsApp daily summary."""
77
+ normalized = (language or "English").strip().lower()
78
+ labels = {
79
+ "english": {
80
+ "daily_summary": "Daily Summary",
81
+ "sales": "Sales",
82
+ "expenses": "Expenses",
83
+ "profit": "Profit",
84
+ "outstanding_credit": "Outstanding Credit",
85
+ "top_product": "Top Product",
86
+ "low_stock": "Low Stock",
87
+ },
88
+ "spanish": {
89
+ "daily_summary": "Resumen Diario",
90
+ "sales": "Ventas",
91
+ "expenses": "Gastos",
92
+ "profit": "Ganancia",
93
+ "outstanding_credit": "Credito Pendiente",
94
+ "top_product": "Producto Principal",
95
+ "low_stock": "Bajo Stock",
96
+ },
97
+ "french": {
98
+ "daily_summary": "Resume Quotidien",
99
+ "sales": "Ventes",
100
+ "expenses": "Depenses",
101
+ "profit": "Profit",
102
+ "outstanding_credit": "Credit En Attente",
103
+ "top_product": "Meilleur Produit",
104
+ "low_stock": "Stock Bas",
105
+ },
106
+ "portuguese": {
107
+ "daily_summary": "Resumo Diario",
108
+ "sales": "Vendas",
109
+ "expenses": "Despesas",
110
+ "profit": "Lucro",
111
+ "outstanding_credit": "Credito Pendente",
112
+ "top_product": "Produto Principal",
113
+ "low_stock": "Estoque Baixo",
114
+ },
115
+ }
116
+ return labels.get(normalized, labels["english"])
voiceledger/ui/gradio_app.py CHANGED
@@ -28,6 +28,7 @@ from voiceledger.ledger.analytics import (
28
  top_selling_items,
29
  )
30
  from voiceledger.ledger.customers import get_customer_balances
 
31
  from voiceledger.ledger.database import (
32
  add_transaction,
33
  delete_transaction,
@@ -68,6 +69,16 @@ LANGUAGE_STYLE_CHOICES = [
68
  "Portuguese",
69
  "Multilingual",
70
  ]
 
 
 
 
 
 
 
 
 
 
71
  DEMO_NOTES = [
72
  "Bought 60 mangoes",
73
  "Sold 12 mangoes, 20 each",
@@ -128,11 +139,22 @@ def create_app(db_path: str | Path | None = None) -> gr.Blocks:
128
  value=initial_settings["business_name"],
129
  elem_classes="vl-panel",
130
  )
 
 
 
 
 
 
131
  currency_symbol_input = gr.Textbox(
132
  label="Currency label",
133
  value=initial_settings["currency_symbol"],
134
  elem_classes="vl-panel",
135
  )
 
 
 
 
 
136
  with gr.Row():
137
  low_stock_threshold_input = gr.Number(
138
  label="Low-stock threshold",
@@ -186,6 +208,15 @@ def create_app(db_path: str | Path | None = None) -> gr.Blocks:
186
  gr.HTML(
187
  _multilingual_examples_panel()
188
  )
 
 
 
 
 
 
 
 
 
189
  with gr.Row(elem_classes="vl-example-row"):
190
  example_sale_button = gr.Button("Try sale")
191
  example_expense_button = gr.Button("Try expense")
@@ -298,6 +329,11 @@ def create_app(db_path: str | Path | None = None) -> gr.Blocks:
298
  inputs=None,
299
  outputs=[note_input, record_demo_status],
300
  )
 
 
 
 
 
301
  apply_review_edits_button.click(
302
  fn=lambda parsed, transaction_type, item, quantity, unit_price, amount, customer, payment_status, notes, confidence: _apply_review_edits(
303
  parsed,
@@ -505,6 +541,21 @@ def create_app(db_path: str | Path | None = None) -> gr.Blocks:
505
  value=_field_test_summary(initial_settings),
506
  elem_classes="vl-status",
507
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
508
 
509
  with gr.Column(visible=False, elem_classes="vl-page-section") as bulk_page:
510
  gr.HTML('<div id="vl-page-bulk" class="vl-page-anchor"></div>')
@@ -700,6 +751,12 @@ def create_app(db_path: str | Path | None = None) -> gr.Blocks:
700
  )
701
  gr.HTML(_section_heading("WhatsApp Summary"))
702
  generate_whatsapp_button = gr.Button("Generate WhatsApp Summary")
 
 
 
 
 
 
703
  whatsapp_summary_output = gr.Textbox(
704
  label="WhatsApp Summary",
705
  lines=10,
@@ -717,8 +774,8 @@ def create_app(db_path: str | Path | None = None) -> gr.Blocks:
717
  """
718
  )
719
  generate_whatsapp_button.click(
720
- fn=lambda: _generate_whatsapp_summary(db_path),
721
- inputs=None,
722
  outputs=whatsapp_summary_output,
723
  )
724
 
@@ -1249,6 +1306,20 @@ def _ai_mode_status(ai_mode: str | None) -> str:
1249
  """
1250
 
1251
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1252
  def _demo_health_placeholder() -> pd.DataFrame:
1253
  """Return placeholder health rows so the demo health table is never blank."""
1254
  return pd.DataFrame(
@@ -1395,6 +1466,7 @@ def _apply_review_edits(
1395
  confidence=confidence,
1396
  )
1397
  payload = transaction.model_dump()
 
1398
  warnings = _review_warnings(transaction, db_path)
1399
  status = _status_message(transaction, "Review updated from your edits.", warnings=warnings)
1400
  return payload, payload, status, _review_card(transaction, "Review updated from your edits.", warnings)
@@ -1946,6 +2018,36 @@ def _get_inventory_detail(item: str | None, db_path: str | Path | None) -> tuple
1946
  return summary, matches[columns].reset_index(drop=True)
1947
 
1948
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1949
  def _generate_daily_summary_report(db_path: str | Path | None) -> tuple[str | None, str]:
1950
  """Generate the Daily Summary PDF for download in Gradio."""
1951
  settings = get_business_settings(db_path)
@@ -2093,7 +2195,7 @@ def _metric_card(label: str, value: str, note: str, profit: float | None = None)
2093
  """
2094
 
2095
 
2096
- def _generate_whatsapp_summary(db_path: str | Path | None) -> str:
2097
  """Generate a WhatsApp summary using seller settings."""
2098
  settings = get_business_settings(db_path)
2099
  return generate_whatsapp_summary(
@@ -2101,6 +2203,7 @@ def _generate_whatsapp_summary(db_path: str | Path | None) -> str:
2101
  low_stock_threshold=get_low_stock_threshold(db_path),
2102
  business_name=settings["business_name"],
2103
  currency_symbol=settings["currency_symbol"],
 
2104
  )
2105
 
2106
 
@@ -2327,6 +2430,7 @@ def _status_message(
2327
  """Return a human-readable parsing status."""
2328
  parts = []
2329
  parts.append(_source_chip(source_message, fallback_reason))
 
2330
  if prefix:
2331
  parts.append(prefix)
2332
  parts.append(source_message)
@@ -2351,6 +2455,29 @@ def _source_chip(source_message: str, fallback_reason: str | None = None) -> str
2351
  return '<span class="vl-status-chip">Parsed</span>'
2352
 
2353
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2354
  def _transcription_status(result: modal_api.TranscriptionResult) -> str:
2355
  """Return a concise transcription source status."""
2356
  status = result.message
 
28
  top_selling_items,
29
  )
30
  from voiceledger.ledger.customers import get_customer_balances
31
+ from voiceledger.ledger.corrections import get_correction_log, record_correction
32
  from voiceledger.ledger.database import (
33
  add_transaction,
34
  delete_transaction,
 
69
  "Portuguese",
70
  "Multilingual",
71
  ]
72
+ WHATSAPP_LANGUAGE_CHOICES = ["English", "Spanish", "French", "Portuguese"]
73
+ CURRENCY_PRESETS = {
74
+ "India - INR (₹)": "₹",
75
+ "United States - USD ($)": "$",
76
+ "European Union - EUR (€)": "€",
77
+ "United Kingdom - GBP (£)": "£",
78
+ "Mexico - MXN ($)": "$",
79
+ "Brazil - BRL (R$)": "R$",
80
+ "Custom": "",
81
+ }
82
  DEMO_NOTES = [
83
  "Bought 60 mangoes",
84
  "Sold 12 mangoes, 20 each",
 
139
  value=initial_settings["business_name"],
140
  elem_classes="vl-panel",
141
  )
142
+ currency_preset_input = gr.Dropdown(
143
+ choices=list(CURRENCY_PRESETS.keys()),
144
+ label="Currency preset",
145
+ value=_currency_preset_for_symbol(initial_settings["currency_symbol"]),
146
+ elem_classes="vl-panel",
147
+ )
148
  currency_symbol_input = gr.Textbox(
149
  label="Currency label",
150
  value=initial_settings["currency_symbol"],
151
  elem_classes="vl-panel",
152
  )
153
+ currency_preset_input.change(
154
+ fn=_currency_symbol_for_preset,
155
+ inputs=currency_preset_input,
156
+ outputs=currency_symbol_input,
157
+ )
158
  with gr.Row():
159
  low_stock_threshold_input = gr.Number(
160
  label="Low-stock threshold",
 
208
  gr.HTML(
209
  _multilingual_examples_panel()
210
  )
211
+ gr.HTML(_section_heading("Voice Command Shortcuts"))
212
+ with gr.Row():
213
+ command_input = gr.Textbox(
214
+ label="Command",
215
+ placeholder="close today, show Amit, stock mangoes",
216
+ elem_classes="vl-panel",
217
+ )
218
+ command_button = gr.Button("Run Command")
219
+ command_output = gr.HTML(_empty_detail_card("Command result", "Try close today, show Amit, or stock mangoes."))
220
  with gr.Row(elem_classes="vl-example-row"):
221
  example_sale_button = gr.Button("Try sale")
222
  example_expense_button = gr.Button("Try expense")
 
329
  inputs=None,
330
  outputs=[note_input, record_demo_status],
331
  )
332
+ command_button.click(
333
+ fn=lambda command: _run_voice_command(command, db_path),
334
+ inputs=command_input,
335
+ outputs=command_output,
336
+ )
337
  apply_review_edits_button.click(
338
  fn=lambda parsed, transaction_type, item, quantity, unit_price, amount, customer, payment_status, notes, confidence: _apply_review_edits(
339
  parsed,
 
541
  value=_field_test_summary(initial_settings),
542
  elem_classes="vl-status",
543
  )
544
+ gr.HTML(_section_heading("Mistake Log"))
545
+ refresh_corrections_button = gr.Button("Refresh Correction Log")
546
+ correction_log_output = gr.Dataframe(
547
+ value=get_correction_log(db_path),
548
+ headers=["id", "changed_fields", "original_payload", "corrected_payload", "created_at"],
549
+ label="Corrections made before save",
550
+ interactive=False,
551
+ wrap=True,
552
+ elem_classes="vl-panel",
553
+ )
554
+ refresh_corrections_button.click(
555
+ fn=lambda: get_correction_log(db_path),
556
+ inputs=None,
557
+ outputs=correction_log_output,
558
+ )
559
 
560
  with gr.Column(visible=False, elem_classes="vl-page-section") as bulk_page:
561
  gr.HTML('<div id="vl-page-bulk" class="vl-page-anchor"></div>')
 
751
  )
752
  gr.HTML(_section_heading("WhatsApp Summary"))
753
  generate_whatsapp_button = gr.Button("Generate WhatsApp Summary")
754
+ whatsapp_language_input = gr.Dropdown(
755
+ choices=WHATSAPP_LANGUAGE_CHOICES,
756
+ label="Summary language",
757
+ value="English",
758
+ elem_classes="vl-panel",
759
+ )
760
  whatsapp_summary_output = gr.Textbox(
761
  label="WhatsApp Summary",
762
  lines=10,
 
774
  """
775
  )
776
  generate_whatsapp_button.click(
777
+ fn=lambda language: _generate_whatsapp_summary(db_path, language),
778
+ inputs=whatsapp_language_input,
779
  outputs=whatsapp_summary_output,
780
  )
781
 
 
1306
  """
1307
 
1308
 
1309
+ def _currency_symbol_for_preset(preset: str | None) -> str:
1310
+ """Return the currency symbol for a selected preset."""
1311
+ return CURRENCY_PRESETS.get(preset or "Custom", "")
1312
+
1313
+
1314
+ def _currency_preset_for_symbol(symbol: str | None) -> str:
1315
+ """Return a likely currency preset for an existing symbol."""
1316
+ cleaned = (symbol or "").strip()
1317
+ for preset, preset_symbol in CURRENCY_PRESETS.items():
1318
+ if preset != "Custom" and preset_symbol == cleaned:
1319
+ return preset
1320
+ return "Custom"
1321
+
1322
+
1323
  def _demo_health_placeholder() -> pd.DataFrame:
1324
  """Return placeholder health rows so the demo health table is never blank."""
1325
  return pd.DataFrame(
 
1466
  confidence=confidence,
1467
  )
1468
  payload = transaction.model_dump()
1469
+ record_correction(transaction_payload, payload, db_path)
1470
  warnings = _review_warnings(transaction, db_path)
1471
  status = _status_message(transaction, "Review updated from your edits.", warnings=warnings)
1472
  return payload, payload, status, _review_card(transaction, "Review updated from your edits.", warnings)
 
2018
  return summary, matches[columns].reset_index(drop=True)
2019
 
2020
 
2021
+ def _run_voice_command(command: str | None, db_path: str | Path | None) -> str:
2022
+ """Run a lightweight text/voice command shortcut."""
2023
+ cleaned = " ".join(str(command or "").split()).strip()
2024
+ if not cleaned:
2025
+ return _empty_detail_card("Command result", "Try close today, show Amit, or stock mangoes.")
2026
+
2027
+ lowered = cleaned.lower()
2028
+ if lowered in {"close today", "daily closeout", "closeout", "summary"}:
2029
+ summary, pdf_path, csv_path, whatsapp, status = _run_daily_closeout(db_path)
2030
+ file_note = f"PDF: {pdf_path or 'needs attention'}. CSV: {csv_path or 'needs attention'}."
2031
+ return f"{summary}<section class=\"vl-detail-card\"><h2>Command result</h2><p>{escape(status)} {escape(file_note)}</p><pre>{escape(whatsapp)}</pre></section>"
2032
+
2033
+ if lowered.startswith(("show ", "customer ")):
2034
+ customer = cleaned.split(" ", 1)[1] if " " in cleaned else ""
2035
+ summary, _ = _get_customer_detail(customer, db_path)
2036
+ return summary
2037
+
2038
+ if lowered.startswith(("stock ", "inventory ")):
2039
+ item = cleaned.split(" ", 1)[1] if " " in cleaned else ""
2040
+ summary, _ = _get_inventory_detail(item, db_path)
2041
+ return summary
2042
+
2043
+ parsed = local_parse_transaction(cleaned)
2044
+ if parsed.transaction_type != "unknown":
2045
+ warnings = _review_warnings(parsed, db_path)
2046
+ return _review_card(parsed, "Command parsed as a transaction. Copy it into the transaction note to save.", warnings)
2047
+
2048
+ return _empty_detail_card("Command result", "Command not recognized. Try close today, show Amit, stock mangoes, or a transaction note.")
2049
+
2050
+
2051
  def _generate_daily_summary_report(db_path: str | Path | None) -> tuple[str | None, str]:
2052
  """Generate the Daily Summary PDF for download in Gradio."""
2053
  settings = get_business_settings(db_path)
 
2195
  """
2196
 
2197
 
2198
+ def _generate_whatsapp_summary(db_path: str | Path | None, language: str = "English") -> str:
2199
  """Generate a WhatsApp summary using seller settings."""
2200
  settings = get_business_settings(db_path)
2201
  return generate_whatsapp_summary(
 
2203
  low_stock_threshold=get_low_stock_threshold(db_path),
2204
  business_name=settings["business_name"],
2205
  currency_symbol=settings["currency_symbol"],
2206
+ language=language,
2207
  )
2208
 
2209
 
 
2430
  """Return a human-readable parsing status."""
2431
  parts = []
2432
  parts.append(_source_chip(source_message, fallback_reason))
2433
+ parts.append(_language_confidence_chip(transaction))
2434
  if prefix:
2435
  parts.append(prefix)
2436
  parts.append(source_message)
 
2455
  return '<span class="vl-status-chip">Parsed</span>'
2456
 
2457
 
2458
+ def _language_confidence_chip(transaction: Transaction) -> str:
2459
+ """Return a compact language/confidence chip for parse status."""
2460
+ language = _detect_note_language(transaction.notes)
2461
+ confidence_label = "High confidence" if transaction.confidence >= 0.85 else "Needs review"
2462
+ return f'<span class="vl-status-chip vl-status-chip-language">{escape(language)} · {confidence_label}</span>'
2463
+
2464
+
2465
+ def _detect_note_language(note: str | None) -> str:
2466
+ """Detect a lightweight language label from common seller note phrases."""
2467
+ text = (note or "").lower()
2468
+ if any(token in text for token in ("vendí", "pagué", " debe ", " pagó", " cada uno", "suministros")):
2469
+ return "Spanish"
2470
+ if any(token in text for token in ("vendu", "payé", " doit ", " a payé", "chacun", "fournitures", "acheté")):
2471
+ return "French"
2472
+ if any(token in text for token in ("vendi", "paguei", " deve ", "pagou", "suprimentos", "comprei")):
2473
+ return "Portuguese"
2474
+ if any(token in text for token in ("dene hai", "dena hai", "diya", "kharida", "chukaya")):
2475
+ return "Hinglish"
2476
+ if any(token in text for token in ("lidha", "aapva che", "apvana che", "aapya")):
2477
+ return "Gujarati-lite"
2478
+ return "English"
2479
+
2480
+
2481
  def _transcription_status(result: modal_api.TranscriptionResult) -> str:
2482
  """Return a concise transcription source status."""
2483
  status = result.message
voiceledger/ui/theme.py CHANGED
@@ -637,6 +637,23 @@ body,
637
  color: #5f370e !important;
638
  }
639
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
640
  .vl-chip {
641
  background: #2c2926 !important;
642
  border-radius: 6px !important;
 
637
  color: #5f370e !important;
638
  }
639
 
640
+ .vl-status-chip-language {
641
+ background: #f0eaff !important;
642
+ border-color: #bda8ef !important;
643
+ color: #43236d !important;
644
+ }
645
+
646
+ .vl-detail-card pre {
647
+ background: #f6f7f2 !important;
648
+ border: 1px solid var(--vl-border) !important;
649
+ border-radius: 10px !important;
650
+ color: var(--vl-text) !important;
651
+ font-size: 13px !important;
652
+ overflow-x: auto !important;
653
+ padding: 10px !important;
654
+ white-space: pre-wrap !important;
655
+ }
656
+
657
  .vl-chip {
658
  background: #2c2926 !important;
659
  border-radius: 6px !important;