deeptutor-knowledge 1.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- deeptutor/api/routers/knowledge.py +5225 -0
- deeptutor/api/routers/marginnote4.py +278 -0
- deeptutor/capabilities/ima/__init__.py +6 -0
- deeptutor/capabilities/ima/binding.py +118 -0
- deeptutor/capabilities/ima/capability.py +92 -0
- deeptutor/capabilities/ima/prompts/en/system.md +33 -0
- deeptutor/capabilities/ima/prompts/zh/system.md +24 -0
- deeptutor/capabilities/ima/tools.py +476 -0
- deeptutor/capabilities/marginnote4/__init__.py +13 -0
- deeptutor/capabilities/marginnote4/binding.py +66 -0
- deeptutor/capabilities/marginnote4/capability.py +80 -0
- deeptutor/capabilities/marginnote4/models.py +173 -0
- deeptutor/capabilities/marginnote4/prompts/en/system.md +54 -0
- deeptutor/capabilities/marginnote4/prompts/zh/system.md +44 -0
- deeptutor/capabilities/marginnote4/store.py +562 -0
- deeptutor/capabilities/marginnote4/tools.py +329 -0
- deeptutor/capabilities/obsidian/__init__.py +6 -0
- deeptutor/capabilities/obsidian/binding.py +75 -0
- deeptutor/capabilities/obsidian/capability.py +82 -0
- deeptutor/capabilities/obsidian/prompts/en/system.md +32 -0
- deeptutor/capabilities/obsidian/prompts/zh/system.md +28 -0
- deeptutor/capabilities/obsidian/tools.py +399 -0
- deeptutor/capabilities/obsidian/vault.py +341 -0
- deeptutor/knowledge/__init__.py +28 -0
- deeptutor/knowledge/add_documents.py +734 -0
- deeptutor/knowledge/indexing_run.py +432 -0
- deeptutor/knowledge/initializer.py +448 -0
- deeptutor/knowledge/manager.py +2570 -0
- deeptutor/knowledge/module_service.py +29 -0
- deeptutor/knowledge/naming.py +40 -0
- deeptutor/knowledge/progress_events.py +45 -0
- deeptutor/knowledge/progress_tracker.py +355 -0
- deeptutor/knowledge/staging.py +69 -0
- deeptutor/services/config/knowledge_base_config.py +239 -0
- deeptutor/services/embedding/__init__.py +46 -0
- deeptutor/services/embedding/adapters/__init__.py +40 -0
- deeptutor/services/embedding/adapters/base.py +196 -0
- deeptutor/services/embedding/adapters/cohere.py +176 -0
- deeptutor/services/embedding/adapters/dashscope_native.py +242 -0
- deeptutor/services/embedding/adapters/gemini.py +462 -0
- deeptutor/services/embedding/adapters/jina.py +161 -0
- deeptutor/services/embedding/adapters/ollama.py +145 -0
- deeptutor/services/embedding/adapters/openai_compatible.py +406 -0
- deeptutor/services/embedding/adapters/openai_sdk.py +150 -0
- deeptutor/services/embedding/client.py +323 -0
- deeptutor/services/embedding/config.py +114 -0
- deeptutor/services/embedding/request_options.py +46 -0
- deeptutor/services/embedding/validation.py +128 -0
- deeptutor/services/github_source/__init__.py +5 -0
- deeptutor/services/github_source/client.py +164 -0
- deeptutor/services/github_source/sync.py +384 -0
- deeptutor/services/github_source/sync_service.py +72 -0
- deeptutor/services/rag/__init__.py +21 -0
- deeptutor/services/rag/embedding_binding.py +331 -0
- deeptutor/services/rag/embedding_signature.py +63 -0
- deeptutor/services/rag/eval/__init__.py +92 -0
- deeptutor/services/rag/eval/dataset.py +244 -0
- deeptutor/services/rag/eval/diff.py +312 -0
- deeptutor/services/rag/eval/matching.py +218 -0
- deeptutor/services/rag/eval/metrics.py +213 -0
- deeptutor/services/rag/eval/report.py +111 -0
- deeptutor/services/rag/eval/runner.py +309 -0
- deeptutor/services/rag/factory.py +369 -0
- deeptutor/services/rag/file_routing.py +5 -0
- deeptutor/services/rag/index_probe.py +327 -0
- deeptutor/services/rag/index_versioning.py +387 -0
- deeptutor/services/rag/kb_paths.py +50 -0
- deeptutor/services/rag/linked_kb.py +237 -0
- deeptutor/services/rag/pipelines/__init__.py +7 -0
- deeptutor/services/rag/pipelines/base.py +41 -0
- deeptutor/services/rag/pipelines/graphrag/__init__.py +18 -0
- deeptutor/services/rag/pipelines/graphrag/compatibility.py +42 -0
- deeptutor/services/rag/pipelines/graphrag/completion_adapter.py +251 -0
- deeptutor/services/rag/pipelines/graphrag/config.py +357 -0
- deeptutor/services/rag/pipelines/graphrag/engine.py +479 -0
- deeptutor/services/rag/pipelines/graphrag/errors.py +328 -0
- deeptutor/services/rag/pipelines/graphrag/ingestion.py +135 -0
- deeptutor/services/rag/pipelines/graphrag/pandas_compat.py +68 -0
- deeptutor/services/rag/pipelines/graphrag/pipeline.py +284 -0
- deeptutor/services/rag/pipelines/graphrag/provider.py +134 -0
- deeptutor/services/rag/pipelines/graphrag/storage.py +127 -0
- deeptutor/services/rag/pipelines/ima/__init__.py +28 -0
- deeptutor/services/rag/pipelines/ima/client.py +364 -0
- deeptutor/services/rag/pipelines/ima/config.py +140 -0
- deeptutor/services/rag/pipelines/ima/envelope.py +109 -0
- deeptutor/services/rag/pipelines/ima/inventory.py +209 -0
- deeptutor/services/rag/pipelines/ima/media.py +190 -0
- deeptutor/services/rag/pipelines/ima/models.py +348 -0
- deeptutor/services/rag/pipelines/ima/notes.py +240 -0
- deeptutor/services/rag/pipelines/ima/pipeline.py +238 -0
- deeptutor/services/rag/pipelines/ima/probe.py +105 -0
- deeptutor/services/rag/pipelines/ima/sources.py +105 -0
- deeptutor/services/rag/pipelines/ima/transport.py +130 -0
- deeptutor/services/rag/pipelines/kiwix/__init__.py +1 -0
- deeptutor/services/rag/pipelines/kiwix/client.py +369 -0
- deeptutor/services/rag/pipelines/kiwix/pipeline.py +75 -0
- deeptutor/services/rag/pipelines/lightrag/README.md +78 -0
- deeptutor/services/rag/pipelines/lightrag/__init__.py +15 -0
- deeptutor/services/rag/pipelines/lightrag/block_policy.py +266 -0
- deeptutor/services/rag/pipelines/lightrag/cache_reuse.py +137 -0
- deeptutor/services/rag/pipelines/lightrag/config.py +453 -0
- deeptutor/services/rag/pipelines/lightrag/engine.py +536 -0
- deeptutor/services/rag/pipelines/lightrag/indexing_policy.py +696 -0
- deeptutor/services/rag/pipelines/lightrag/ingress.py +364 -0
- deeptutor/services/rag/pipelines/lightrag/parser.py +99 -0
- deeptutor/services/rag/pipelines/lightrag/pipeline.py +692 -0
- deeptutor/services/rag/pipelines/lightrag/roles.py +132 -0
- deeptutor/services/rag/pipelines/lightrag/sidecar.py +320 -0
- deeptutor/services/rag/pipelines/lightrag/storage.py +338 -0
- deeptutor/services/rag/pipelines/lightrag/worker.py +259 -0
- deeptutor/services/rag/pipelines/lightrag/write_lock.py +55 -0
- deeptutor/services/rag/pipelines/lightrag_server/__init__.py +15 -0
- deeptutor/services/rag/pipelines/lightrag_server/client.py +149 -0
- deeptutor/services/rag/pipelines/lightrag_server/config.py +68 -0
- deeptutor/services/rag/pipelines/lightrag_server/pipeline.py +120 -0
- deeptutor/services/rag/pipelines/lightrag_server/probe.py +110 -0
- deeptutor/services/rag/pipelines/llamaindex/__init__.py +3 -0
- deeptutor/services/rag/pipelines/llamaindex/config.py +199 -0
- deeptutor/services/rag/pipelines/llamaindex/document_loader.py +648 -0
- deeptutor/services/rag/pipelines/llamaindex/embedding_adapter.py +264 -0
- deeptutor/services/rag/pipelines/llamaindex/errors.py +76 -0
- deeptutor/services/rag/pipelines/llamaindex/exercise_lookup.py +373 -0
- deeptutor/services/rag/pipelines/llamaindex/ingestion.py +129 -0
- deeptutor/services/rag/pipelines/llamaindex/pipeline.py +556 -0
- deeptutor/services/rag/pipelines/llamaindex/rerank.py +130 -0
- deeptutor/services/rag/pipelines/llamaindex/retrievers.py +360 -0
- deeptutor/services/rag/pipelines/llamaindex/storage.py +655 -0
- deeptutor/services/rag/pipelines/llamaindex/vector_store.py +288 -0
- deeptutor/services/rag/pipelines/modes.py +48 -0
- deeptutor/services/rag/pipelines/pageindex/__init__.py +47 -0
- deeptutor/services/rag/pipelines/pageindex/client.py +146 -0
- deeptutor/services/rag/pipelines/pageindex/config.py +49 -0
- deeptutor/services/rag/pipelines/pageindex/pipeline.py +313 -0
- deeptutor/services/rag/pipelines/pageindex/reasoning.py +262 -0
- deeptutor/services/rag/pipelines/pageindex/storage.py +133 -0
- deeptutor/services/rag/pipelines/pageindex/tools.py +221 -0
- deeptutor/services/rag/pipelines/weknora/__init__.py +13 -0
- deeptutor/services/rag/pipelines/weknora/client.py +106 -0
- deeptutor/services/rag/pipelines/weknora/config.py +56 -0
- deeptutor/services/rag/pipelines/weknora/pipeline.py +74 -0
- deeptutor/services/rag/pipelines/weknora/probe.py +80 -0
- deeptutor/services/rag/preflight.py +274 -0
- deeptutor/services/rag/provider_binding.py +70 -0
- deeptutor/services/rag/query_rewrite.py +186 -0
- deeptutor/services/rag/service.py +379 -0
- deeptutor/services/rag/smart_retriever.py +81 -0
- deeptutor/services/rag/source_pages.py +292 -0
- deeptutor/services/rag/source_visuals.py +434 -0
- deeptutor/services/rag/visual_assets.py +430 -0
- deeptutor/services/rag/visual_coverage.py +165 -0
- deeptutor/services/web_source/__init__.py +1 -0
- deeptutor/services/web_source/bilingual.py +255 -0
- deeptutor/services/web_source/crawler.py +681 -0
- deeptutor/services/web_source/html_extractor.py +643 -0
- deeptutor/services/web_source/markdown.py +28 -0
- deeptutor/services/web_source/navigation.py +72 -0
- deeptutor/services/web_source/repository.py +640 -0
- deeptutor/services/web_source/robots.py +182 -0
- deeptutor/services/web_source/scheduler.py +336 -0
- deeptutor/services/web_source/snapshot_assets.py +171 -0
- deeptutor/services/web_source/sync.py +253 -0
- deeptutor/services/workspace/kb_move.py +446 -0
- deeptutor/tools/rag_tool.py +110 -0
- deeptutor_cli/kb.py +756 -0
- deeptutor_knowledge-1.7.0.dist-info/METADATA +15 -0
- deeptutor_knowledge-1.7.0.dist-info/RECORD +168 -0
- deeptutor_knowledge-1.7.0.dist-info/WHEEL +5 -0
- deeptutor_knowledge-1.7.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"""Bilingual source pairing and language variant alignment for web sources.
|
|
2
|
+
|
|
3
|
+
Detects language variants (via hreflang links or URL language patterns),
|
|
4
|
+
associates variant pages with canonical source pages, and prepares pairing
|
|
5
|
+
records for storage and API exposure.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import hashlib
|
|
11
|
+
from typing import Any
|
|
12
|
+
from urllib.parse import urlparse
|
|
13
|
+
|
|
14
|
+
# Canonical supported language codes and their common regional/script aliases.
|
|
15
|
+
_CANONICAL_LANGUAGES: dict[str, str] = {
|
|
16
|
+
"en": "en",
|
|
17
|
+
"en-us": "en",
|
|
18
|
+
"en-gb": "en",
|
|
19
|
+
"en-au": "en",
|
|
20
|
+
"en-ca": "en",
|
|
21
|
+
"zh": "zh",
|
|
22
|
+
"zh-cn": "zh",
|
|
23
|
+
"zh-hans": "zh",
|
|
24
|
+
"zh-sg": "zh",
|
|
25
|
+
"zh-tw": "zh",
|
|
26
|
+
"zh-hant": "zh",
|
|
27
|
+
"zh-hk": "zh",
|
|
28
|
+
"ja": "ja",
|
|
29
|
+
"ja-jp": "ja",
|
|
30
|
+
"ko": "ko",
|
|
31
|
+
"ko-kr": "ko",
|
|
32
|
+
"fr": "fr",
|
|
33
|
+
"fr-fr": "fr",
|
|
34
|
+
"fr-ca": "fr",
|
|
35
|
+
"de": "de",
|
|
36
|
+
"de-de": "de",
|
|
37
|
+
"de-at": "de",
|
|
38
|
+
"de-ch": "de",
|
|
39
|
+
"es": "es",
|
|
40
|
+
"es-es": "es",
|
|
41
|
+
"es-419": "es",
|
|
42
|
+
"es-mx": "es",
|
|
43
|
+
"ru": "ru",
|
|
44
|
+
"ru-ru": "ru",
|
|
45
|
+
"pt": "pt",
|
|
46
|
+
"pt-br": "pt",
|
|
47
|
+
"pt-pt": "pt",
|
|
48
|
+
"it": "it",
|
|
49
|
+
"it-it": "it",
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
_KNOWN_LANG_CODES = frozenset(_CANONICAL_LANGUAGES.keys())
|
|
53
|
+
|
|
54
|
+
SUPPORTED_LANGUAGE_PAIRS = frozenset(
|
|
55
|
+
{
|
|
56
|
+
frozenset({"en", "zh"}),
|
|
57
|
+
frozenset({"en", "ja"}),
|
|
58
|
+
frozenset({"en", "ko"}),
|
|
59
|
+
frozenset({"en", "fr"}),
|
|
60
|
+
frozenset({"en", "de"}),
|
|
61
|
+
frozenset({"en", "es"}),
|
|
62
|
+
frozenset({"en", "ru"}),
|
|
63
|
+
frozenset({"en", "pt"}),
|
|
64
|
+
frozenset({"en", "it"}),
|
|
65
|
+
}
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def normalize_language(code: str | None) -> str:
|
|
70
|
+
"""Normalize a language tag to its canonical primary language code.
|
|
71
|
+
|
|
72
|
+
Examples:
|
|
73
|
+
'zh-CN' -> 'zh'
|
|
74
|
+
'en-US' -> 'en'
|
|
75
|
+
'zh-Hans' -> 'zh'
|
|
76
|
+
'FR' -> 'fr'
|
|
77
|
+
'x-default' -> ''
|
|
78
|
+
"""
|
|
79
|
+
if not code:
|
|
80
|
+
return ""
|
|
81
|
+
cleaned = code.strip().lower().replace("_", "-")
|
|
82
|
+
if cleaned in ("x-default", "default"):
|
|
83
|
+
return ""
|
|
84
|
+
if cleaned in _CANONICAL_LANGUAGES:
|
|
85
|
+
return _CANONICAL_LANGUAGES[cleaned]
|
|
86
|
+
primary = cleaned.split("-", 1)[0]
|
|
87
|
+
return _CANONICAL_LANGUAGES.get(primary, primary)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def is_supported_pair(lang_a: str, lang_b: str) -> bool:
|
|
91
|
+
"""Check if two languages form a supported bilingual pairing."""
|
|
92
|
+
if not lang_a or not lang_b or lang_a == lang_b:
|
|
93
|
+
return False
|
|
94
|
+
pair = frozenset({lang_a, lang_b})
|
|
95
|
+
if pair in SUPPORTED_LANGUAGE_PAIRS:
|
|
96
|
+
return True
|
|
97
|
+
return "en" in pair or "zh" in pair
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def extract_url_language_and_stem(url: str) -> tuple[str, str]:
|
|
101
|
+
"""Extract language code (if in URL path) and the language-agnostic stem path.
|
|
102
|
+
|
|
103
|
+
Handles leading language segments (/zh/docs/intro -> ("zh", "/docs/intro"))
|
|
104
|
+
and common nested prefixes (/docs/zh/intro -> ("zh", "/docs/intro")).
|
|
105
|
+
"""
|
|
106
|
+
parsed = urlparse(url)
|
|
107
|
+
raw_path = parsed.path.rstrip("/")
|
|
108
|
+
if not raw_path:
|
|
109
|
+
return "", "/"
|
|
110
|
+
|
|
111
|
+
segments = [s for s in raw_path.split("/") if s]
|
|
112
|
+
if not segments:
|
|
113
|
+
return "", "/"
|
|
114
|
+
|
|
115
|
+
first_norm = normalize_language(segments[0])
|
|
116
|
+
if segments[0].lower() in _KNOWN_LANG_CODES or first_norm in SUPPORTED_LANGUAGE_PAIRS:
|
|
117
|
+
stem = "/" + "/".join(segments[1:])
|
|
118
|
+
return first_norm, stem or "/"
|
|
119
|
+
|
|
120
|
+
if len(segments) >= 2:
|
|
121
|
+
second_norm = normalize_language(segments[1])
|
|
122
|
+
if segments[1].lower() in _KNOWN_LANG_CODES or second_norm in SUPPORTED_LANGUAGE_PAIRS:
|
|
123
|
+
stem = f"/{segments[0]}" + ("/" + "/".join(segments[2:]) if len(segments) > 2 else "")
|
|
124
|
+
return second_norm, stem or "/"
|
|
125
|
+
|
|
126
|
+
return "", raw_path or "/"
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def detect_bilingual_pairings(
|
|
130
|
+
pages: list[Any],
|
|
131
|
+
page_file_map: dict[str, str] | None = None,
|
|
132
|
+
) -> list[dict[str, Any]]:
|
|
133
|
+
"""Detect bilingual page pairings among crawled pages.
|
|
134
|
+
|
|
135
|
+
Uses both explicit hreflang alternate declarations and URL language prefix
|
|
136
|
+
patterns. Returns a deduplicated, deterministically sorted list of pairing
|
|
137
|
+
records.
|
|
138
|
+
"""
|
|
139
|
+
if not pages:
|
|
140
|
+
return []
|
|
141
|
+
|
|
142
|
+
file_map = page_file_map or {}
|
|
143
|
+
pages_by_url = {page.url: page for page in pages}
|
|
144
|
+
pairings: dict[str, dict[str, Any]] = {}
|
|
145
|
+
|
|
146
|
+
# 1. Hreflang alternate links (highest confidence)
|
|
147
|
+
for page in pages:
|
|
148
|
+
p1_lang = (
|
|
149
|
+
normalize_language(getattr(page, "language", ""))
|
|
150
|
+
or extract_url_language_and_stem(page.url)[0]
|
|
151
|
+
or "en"
|
|
152
|
+
)
|
|
153
|
+
alternate_links: dict[str, str] = getattr(page, "alternate_links", {})
|
|
154
|
+
for hreflang, target_url in alternate_links.items():
|
|
155
|
+
if target_url not in pages_by_url or target_url == page.url:
|
|
156
|
+
continue
|
|
157
|
+
target_page = pages_by_url[target_url]
|
|
158
|
+
p2_lang = (
|
|
159
|
+
normalize_language(hreflang)
|
|
160
|
+
or normalize_language(getattr(target_page, "language", ""))
|
|
161
|
+
or extract_url_language_and_stem(target_url)[0]
|
|
162
|
+
or "zh"
|
|
163
|
+
)
|
|
164
|
+
if not is_supported_pair(p1_lang, p2_lang):
|
|
165
|
+
continue
|
|
166
|
+
|
|
167
|
+
if p1_lang == "en":
|
|
168
|
+
src, tgt = page, target_page
|
|
169
|
+
s_lang, t_lang = p1_lang, p2_lang
|
|
170
|
+
elif p2_lang == "en":
|
|
171
|
+
src, tgt = target_page, page
|
|
172
|
+
s_lang, t_lang = p2_lang, p1_lang
|
|
173
|
+
elif page.url <= target_page.url:
|
|
174
|
+
src, tgt = page, target_page
|
|
175
|
+
s_lang, t_lang = p1_lang, p2_lang
|
|
176
|
+
else:
|
|
177
|
+
src, tgt = target_page, page
|
|
178
|
+
s_lang, t_lang = p2_lang, p1_lang
|
|
179
|
+
|
|
180
|
+
pairing_id = hashlib.sha256(f"{src.url}::{tgt.url}".encode("utf-8")).hexdigest()[:16]
|
|
181
|
+
pairings[pairing_id] = {
|
|
182
|
+
"pairing_id": pairing_id,
|
|
183
|
+
"source_url": src.url,
|
|
184
|
+
"target_url": tgt.url,
|
|
185
|
+
"source_file": file_map.get(src.url, ""),
|
|
186
|
+
"target_file": file_map.get(tgt.url, ""),
|
|
187
|
+
"source_lang": s_lang,
|
|
188
|
+
"target_lang": t_lang,
|
|
189
|
+
"pairing_method": "hreflang",
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
# 2. URL language stem pattern matching
|
|
193
|
+
stem_groups: dict[str, list[tuple[Any, str]]] = {}
|
|
194
|
+
for page in pages:
|
|
195
|
+
url_lang, stem = extract_url_language_and_stem(page.url)
|
|
196
|
+
page_lang = normalize_language(getattr(page, "language", "")) or url_lang
|
|
197
|
+
stem_groups.setdefault(stem, []).append((page, page_lang))
|
|
198
|
+
|
|
199
|
+
for stem, group in stem_groups.items():
|
|
200
|
+
if len(group) < 2:
|
|
201
|
+
continue
|
|
202
|
+
for i in range(len(group)):
|
|
203
|
+
for j in range(i + 1, len(group)):
|
|
204
|
+
p1, l1 = group[i]
|
|
205
|
+
p2, l2 = group[j]
|
|
206
|
+
|
|
207
|
+
# If one page is unprefixed and the other has a language tag,
|
|
208
|
+
# infer default 'en' for the unprefixed base documentation page.
|
|
209
|
+
if not l1 and l2 and l2 != "en":
|
|
210
|
+
l1 = "en"
|
|
211
|
+
elif not l2 and l1 and l1 != "en":
|
|
212
|
+
l2 = "en"
|
|
213
|
+
|
|
214
|
+
if not is_supported_pair(l1, l2):
|
|
215
|
+
continue
|
|
216
|
+
|
|
217
|
+
if l1 == "en":
|
|
218
|
+
src, tgt = p1, p2
|
|
219
|
+
s_lang, t_lang = l1, l2
|
|
220
|
+
elif l2 == "en":
|
|
221
|
+
src, tgt = p2, p1
|
|
222
|
+
s_lang, t_lang = l2, l1
|
|
223
|
+
elif p1.url <= p2.url:
|
|
224
|
+
src, tgt = p1, p2
|
|
225
|
+
s_lang, t_lang = l1, l2
|
|
226
|
+
else:
|
|
227
|
+
src, tgt = p2, p1
|
|
228
|
+
s_lang, t_lang = l2, l1
|
|
229
|
+
|
|
230
|
+
pairing_id = hashlib.sha256(f"{src.url}::{tgt.url}".encode("utf-8")).hexdigest()[
|
|
231
|
+
:16
|
|
232
|
+
]
|
|
233
|
+
if pairing_id not in pairings:
|
|
234
|
+
pairings[pairing_id] = {
|
|
235
|
+
"pairing_id": pairing_id,
|
|
236
|
+
"source_url": src.url,
|
|
237
|
+
"target_url": tgt.url,
|
|
238
|
+
"source_file": file_map.get(src.url, ""),
|
|
239
|
+
"target_file": file_map.get(tgt.url, ""),
|
|
240
|
+
"source_lang": s_lang,
|
|
241
|
+
"target_lang": t_lang,
|
|
242
|
+
"pairing_method": "url_pattern",
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
result = list(pairings.values())
|
|
246
|
+
result.sort(key=lambda item: (item["source_url"], item["target_url"]))
|
|
247
|
+
return result
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
__all__ = [
|
|
251
|
+
"detect_bilingual_pairings",
|
|
252
|
+
"extract_url_language_and_stem",
|
|
253
|
+
"is_supported_pair",
|
|
254
|
+
"normalize_language",
|
|
255
|
+
]
|