From 99527dfc2b71856af02e896403207727ea99d9c2 Mon Sep 17 00:00:00 2001 From: Mircea Lungu Date: Sun, 2 Aug 2026 01:32:15 +0300 Subject: [PATCH] Friend-share: pre-generate derivative for crawl shares too (same-language) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Completes the multiplexer for feed shares. When there's no upload, generate the recipient's copy by simplifying the CANONICAL crawled article to their level (create_recipient_derivative_from_article -> create_user_specific_ simplified_version, which makes a proper simplified child: parent_url, is_simplified, target_cefr_level all correct). The email deep-link then opens a ready, adapted copy — no 'adapt to your level?' prompt. Cross-language crawl TRANSLATION is deferred (a translated child needs a same-language guard in the recommender overlay first); it returns None there, and the recipient opens the original and adapts, as before. Also None when no simplification is needed (recipient's level >= the article's). Co-Authored-By: Claude Opus 4.8 --- zeeguu/api/endpoints/friends.py | 38 ++++++++++++------- .../simplification_and_classification.py | 33 ++++++++++++++++ 2 files changed, 57 insertions(+), 14 deletions(-) diff --git a/zeeguu/api/endpoints/friends.py b/zeeguu/api/endpoints/friends.py index 753b68c5..d7d766d1 100644 --- a/zeeguu/api/endpoints/friends.py +++ b/zeeguu/api/endpoints/friends.py @@ -467,7 +467,7 @@ def _deliver_share(from_user_id: int, to_user_id: int, article_id: int, note): delivery_language, level = SharedArticle.compute_delivery_language(recipient) delivery_language_id = delivery_language.id if delivery_language is not None else None delivery_article_id = ( - _generate_recipient_derivative(article, recipient, delivery_language, level) + _generate_recipient_derivative(article, original_id, recipient, delivery_language, level) if delivery_language is not None else None ) @@ -486,26 +486,36 @@ def _deliver_share(from_user_id: int, to_user_id: int, article_id: int, note): log(f"share {shared.id}: email notification failed: {e}") -def _generate_recipient_derivative(article, recipient, delivery_language, level): +def _generate_recipient_derivative(article, canonical_id, recipient, delivery_language, level): """The id of the recipient's personalized derivative, or None. - Only for upload-based shares (the sharer captured a full body); plain crawl - shares get None and the recipient opens the canonical article (adapting it - in the reader). Never raises — a generation failure (LLM/fragment/DB) rolls - back and degrades to None so the share still lands, routed by its language. + Two sources: an **upload** (the sharer captured a full body) → generate from + its text; otherwise a **crawled article** → simplify the canonical article to + the recipient's level. Either way the recipient's deep-link opens a ready, + adapted copy — no "adapt to your level?" prompt. Falls back to None (recipient + opens the original and adapts) when nothing needs generating or on any error; + never raises, so the share still lands, routed by its language. """ - upload = article.source_upload - if upload is None: - return None - from zeeguu.core.llm_services.simplification_and_classification import ( create_recipient_derivative, + create_recipient_derivative_from_article, ) try: - derivative = create_recipient_derivative( - db.session, upload, delivery_language.code, level - ) + upload = article.source_upload + if upload is not None: + derivative = create_recipient_derivative( + db.session, upload, delivery_language.code, level + ) + else: + canonical = Article.find_by_id(canonical_id) + derivative = ( + create_recipient_derivative_from_article( + db.session, canonical, delivery_language.code, level + ) + if canonical is not None + else None + ) except Exception as e: db.session.rollback() log(f"share to user_id={recipient.id}: derivative generation errored " @@ -513,7 +523,7 @@ def _generate_recipient_derivative(article, recipient, delivery_language, level) return None if derivative is None: - log(f"share to user_id={recipient.id}: derivative generation failed; " + log(f"share to user_id={recipient.id}: no derivative generated; " f"recipient will open the canonical article") return None diff --git a/zeeguu/core/llm_services/simplification_and_classification.py b/zeeguu/core/llm_services/simplification_and_classification.py index b01f49e0..5941eeec 100644 --- a/zeeguu/core/llm_services/simplification_and_classification.py +++ b/zeeguu/core/llm_services/simplification_and_classification.py @@ -859,6 +859,39 @@ def create_user_specific_simplified_version(session, article, target_level): return None +def create_recipient_derivative_from_article( + session, article, target_language_code, target_level +): + """A recipient's personalized copy generated from a crawled ARTICLE (no upload). + + The multiplexer for *feed* shares: the sharer sent a crawled article, and the + recipient reads it at their level. Reuses the article-simplify path, which + creates a proper simplified child (``parent_article_id`` → the reader's + ``Original:`` link, ``is_simplified``, ``target_cefr_level`` all work) and is + same-language (so it can't confuse the recommender's overlay). + + **MVP scope — same language only.** Cross-language crawl *translation* is + deferred: a translated child would need a same-language guard in the + recommender overlay first, so here we return ``None`` and the recipient opens + the original and adapts it in the reader (today's behavior). Also returns + ``None`` when no simplification is needed (recipient's level ≥ the article's) + — they just read the original. + """ + from zeeguu.core.model.article import Article + + if not article.language: + return None + if target_language_code != article.language.code: + return None # cross-language crawl translation deferred (see docstring) + + existing = Article.query.filter_by( + parent_article_id=article.id, cefr_level=target_level + ).first() + if existing: + return existing + return create_user_specific_simplified_version(session, article, target_level) + + def create_recipient_derivative(session, upload, target_language_code, target_level): """A recipient's personalized full-body copy, generated from a SHARER's upload.