kd-engine 1.0.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.
- kd_engine-1.0.0.dist-info/METADATA +19 -0
- kd_engine-1.0.0.dist-info/RECORD +118 -0
- kd_engine-1.0.0.dist-info/WHEEL +5 -0
- kd_engine-1.0.0.dist-info/entry_points.txt +2 -0
- kd_engine-1.0.0.dist-info/licenses/LICENSE-MIT +21 -0
- kd_engine-1.0.0.dist-info/top_level.txt +1 -0
- ytk/__init__.py +2 -0
- ytk/__main__.py +21 -0
- ytk/batch/__init__.py +39 -0
- ytk/batch/runner.py +1149 -0
- ytk/cache/__init__.py +34 -0
- ytk/cache/v2.py +562 -0
- ytk/cleaning/density.py +40 -0
- ytk/cleaning/fillers.py +18 -0
- ytk/cleaning/sentence_boundary.py +30 -0
- ytk/cleaning/service.py +62 -0
- ytk/cli.py +1281 -0
- ytk/commands/__init__.py +3 -0
- ytk/commands/merge.py +600 -0
- ytk/config.py +757 -0
- ytk/dedup/service.py +166 -0
- ytk/doctor.py +891 -0
- ytk/dotenv.py +334 -0
- ytk/errors.py +38 -0
- ytk/extraction/service.py +451 -0
- ytk/extraction/strategies/base.py +16 -0
- ytk/extraction/strategies/code_block.py +285 -0
- ytk/extraction/strategies/heuristic.py +791 -0
- ytk/extraction/strategies/local_model.py +56 -0
- ytk/extraction/text_context.py +177 -0
- ytk/extraction/visual_context.py +159 -0
- ytk/ingestion/adapters/__init__.py +1 -0
- ytk/ingestion/adapters/_common.py +865 -0
- ytk/ingestion/adapters/base.py +160 -0
- ytk/ingestion/adapters/bilibili.py +271 -0
- ytk/ingestion/adapters/douyin.py +445 -0
- ytk/ingestion/adapters/tiktok.py +102 -0
- ytk/ingestion/adapters/xiaohongshu.py +758 -0
- ytk/ingestion/adapters/youtube.py +74 -0
- ytk/ingestion/browser_acquired.py +220 -0
- ytk/ingestion/http_client.py +43 -0
- ytk/ingestion/local_import.py +202 -0
- ytk/ingestion/media.py +468 -0
- ytk/ingestion/providers/asr.py +902 -0
- ytk/ingestion/providers/base.py +13 -0
- ytk/ingestion/providers/dtk_client.py +816 -0
- ytk/ingestion/providers/minimax.py +350 -0
- ytk/ingestion/providers/transcript_api.py +336 -0
- ytk/ingestion/providers/xhs_client.py +646 -0
- ytk/ingestion/providers/ytdlp.py +400 -0
- ytk/ingestion/registry.py +189 -0
- ytk/ingestion/resolver.py +417 -0
- ytk/ingestion/search.py +56 -0
- ytk/ingestion/service.py +167 -0
- ytk/ingestion/subtitles.py +602 -0
- ytk/ingestion/transcript_cache.py +212 -0
- ytk/ingestion/url_parser.py +53 -0
- ytk/ingestion/video_frames.py +292 -0
- ytk/ingestion/ytdlp_acquire.py +754 -0
- ytk/logging.py +383 -0
- ytk/models/common.py +20 -0
- ytk/models/content.py +451 -0
- ytk/models/knowledge.py +43 -0
- ytk/models/migrations.py +262 -0
- ytk/models/skill.py +13 -0
- ytk/models/source.py +265 -0
- ytk/models/topic.py +18 -0
- ytk/models/transcript.py +30 -0
- ytk/ocr/__init__.py +65 -0
- ytk/ocr/engine.py +245 -0
- ytk/ocr/layout.py +347 -0
- ytk/ocr/pipeline.py +440 -0
- ytk/ocr/rapidocr_engine.py +144 -0
- ytk/pipeline/acquisition_checkpoint.py +240 -0
- ytk/pipeline/artifacts.py +18 -0
- ytk/pipeline/orchestrator.py +598 -0
- ytk/pipeline/processing_identity.py +18 -0
- ytk/pipeline/retry.py +728 -0
- ytk/pipeline/runners.py +578 -0
- ytk/pipeline/state.py +1107 -0
- ytk/pipeline/v2.py +1863 -0
- ytk/py.typed +1 -0
- ytk/quality/__init__.py +9 -0
- ytk/quality/completeness.py +260 -0
- ytk/renderers/base.py +9 -0
- ytk/renderers/citation_alignment.py +617 -0
- ytk/renderers/evidence_formatter.py +362 -0
- ytk/renderers/json_renderer.py +13 -0
- ytk/renderers/jsonl_renderer.py +18 -0
- ytk/renderers/knowledge_doc_renderer.py +1984 -0
- ytk/renderers/markdown_renderer.py +540 -0
- ytk/renderers/obsidian_renderer.py +138 -0
- ytk/renderers/skill_package.py +21 -0
- ytk/renderers/source_links.py +309 -0
- ytk/renderers/summary_renderer.py +163 -0
- ytk/renderers/v2.py +171 -0
- ytk/renderers/v2_jsonl.py +165 -0
- ytk/renderers/v2_markdown.py +477 -0
- ytk/renderers/v2_obsidian.py +113 -0
- ytk/renderers/v2_summary.py +220 -0
- ytk/segmentation/change_points.py +50 -0
- ytk/segmentation/embeddings.py +45 -0
- ytk/segmentation/service.py +318 -0
- ytk/skill_builder/__init__.py +1 -0
- ytk/skill_builder/builder.py +419 -0
- ytk/skill_builder/templates/SKILL.md.j2 +85 -0
- ytk/skill_builder/templates/evals/evals.json.j2 +25 -0
- ytk/skill_builder/templates/examples/example-input.md.j2 +5 -0
- ytk/skill_builder/templates/examples/example-output.md.j2 +8 -0
- ytk/skill_builder/templates/references/commands.md.j2 +34 -0
- ytk/skill_builder/templates/references/concepts.md.j2 +22 -0
- ytk/skill_builder/templates/references/configs.md.j2 +31 -0
- ytk/skill_builder/templates/references/evidence-policy.md.j2 +27 -0
- ytk/skill_builder/templates/references/source-summary.md.j2 +24 -0
- ytk/skill_builder/templates/scripts/build_skill.py.j2 +14 -0
- ytk/skill_builder/templates/scripts/validate_output.py.j2 +136 -0
- ytk/skill_builder/templates.py +29 -0
- ytk/text_normalization.py +44 -0
ytk/dedup/service.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from collections.abc import Iterable
|
|
5
|
+
|
|
6
|
+
from ytk.models.content import KnowledgeAtomV2
|
|
7
|
+
from ytk.models.knowledge import KnowledgeAtom
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _normalize_content(text: str) -> str:
|
|
11
|
+
return re.sub(r"[^\w\s]", "", text.casefold().strip())
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
#: Negation markers. Two atoms whose content differs only by one of these must
|
|
15
|
+
#: never be merged (T41.1: "保留否定、条件和相反结论").
|
|
16
|
+
NEGATION_TOKENS: frozenset[str] = frozenset(
|
|
17
|
+
{
|
|
18
|
+
"not",
|
|
19
|
+
"no",
|
|
20
|
+
"never",
|
|
21
|
+
"none",
|
|
22
|
+
"without",
|
|
23
|
+
"avoid",
|
|
24
|
+
"cannot",
|
|
25
|
+
"cant",
|
|
26
|
+
"can't",
|
|
27
|
+
"dont",
|
|
28
|
+
"don't",
|
|
29
|
+
"doesnt",
|
|
30
|
+
"doesn't",
|
|
31
|
+
"isnt",
|
|
32
|
+
"isn't",
|
|
33
|
+
"arent",
|
|
34
|
+
"aren't",
|
|
35
|
+
"wont",
|
|
36
|
+
"won't",
|
|
37
|
+
"shouldnt",
|
|
38
|
+
"shouldn't",
|
|
39
|
+
"mustnt",
|
|
40
|
+
"mustn't",
|
|
41
|
+
"禁止",
|
|
42
|
+
"不要",
|
|
43
|
+
"不能",
|
|
44
|
+
"不得",
|
|
45
|
+
"无需",
|
|
46
|
+
"避免",
|
|
47
|
+
"不可",
|
|
48
|
+
"严禁",
|
|
49
|
+
}
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
#: Condition / precondition markers. "Do X" and "Do X only if Y" are
|
|
53
|
+
#: different conclusions even when the action text matches.
|
|
54
|
+
CONDITION_TOKENS: frozenset[str] = frozenset(
|
|
55
|
+
{
|
|
56
|
+
"if",
|
|
57
|
+
"unless",
|
|
58
|
+
"when",
|
|
59
|
+
"whenever",
|
|
60
|
+
"only",
|
|
61
|
+
"before",
|
|
62
|
+
"after",
|
|
63
|
+
"provided",
|
|
64
|
+
"otherwise",
|
|
65
|
+
"else",
|
|
66
|
+
"如果",
|
|
67
|
+
"只有",
|
|
68
|
+
"除非",
|
|
69
|
+
"当",
|
|
70
|
+
"之前",
|
|
71
|
+
"之后",
|
|
72
|
+
"否则",
|
|
73
|
+
"前提",
|
|
74
|
+
}
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
_WORD_RE = re.compile(r"[a-z][a-z'\u2019_-]*")
|
|
78
|
+
_CJK_RE = re.compile(r"[\u4e00-\u9fff]+")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _qualifier_tokens(text: str, vocabulary: frozenset[str]) -> frozenset[str]:
|
|
82
|
+
"""Tokens from ``vocabulary`` present in ``text`` (latin words + CJK runs)."""
|
|
83
|
+
lowered = str(text).casefold()
|
|
84
|
+
words = set(_WORD_RE.findall(lowered))
|
|
85
|
+
tokens = {entry for entry in vocabulary if entry in words}
|
|
86
|
+
cjk_entries = [entry for entry in vocabulary if _CJK_RE.search(entry)]
|
|
87
|
+
if cjk_entries:
|
|
88
|
+
runs = _CJK_RE.findall(lowered)
|
|
89
|
+
tokens.update(
|
|
90
|
+
entry for entry in cjk_entries if any(entry in run for run in runs)
|
|
91
|
+
)
|
|
92
|
+
return frozenset(tokens)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _ordered_union(left: Iterable[str], right: Iterable[str]) -> list[str]:
|
|
96
|
+
merged: list[str] = []
|
|
97
|
+
for value in (*left, *right):
|
|
98
|
+
if value not in merged:
|
|
99
|
+
merged.append(value)
|
|
100
|
+
return merged
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class DeduplicationService:
|
|
104
|
+
def __init__(
|
|
105
|
+
self,
|
|
106
|
+
embedding_client: object | None = None,
|
|
107
|
+
semantic_threshold: float = 0.85,
|
|
108
|
+
) -> None:
|
|
109
|
+
self.embedding_client = embedding_client
|
|
110
|
+
self.semantic_threshold = semantic_threshold
|
|
111
|
+
|
|
112
|
+
def merge_v2(self, atoms: list[KnowledgeAtomV2]) -> list[KnowledgeAtomV2]:
|
|
113
|
+
"""Merge v2 atoms by type + normalized content (+ qualifier signature).
|
|
114
|
+
|
|
115
|
+
A merge unions ``source_ids``/``evidence_ids``/``topic_path`` and keeps
|
|
116
|
+
the **maximum** confidence of the inputs. Confidence is never raised
|
|
117
|
+
because more sources carry the same claim (a repost is not evidence),
|
|
118
|
+
and atoms that differ in negation or condition never merge.
|
|
119
|
+
"""
|
|
120
|
+
merged: dict[tuple[object, ...], KnowledgeAtomV2] = {}
|
|
121
|
+
for atom in atoms:
|
|
122
|
+
key: tuple[object, ...] = (
|
|
123
|
+
atom.atom_type,
|
|
124
|
+
_normalize_content(atom.content),
|
|
125
|
+
_qualifier_tokens(atom.content, NEGATION_TOKENS),
|
|
126
|
+
_qualifier_tokens(atom.content, CONDITION_TOKENS),
|
|
127
|
+
)
|
|
128
|
+
if atom.atom_type == "core_concept":
|
|
129
|
+
# Concept content is often only a name. Its definition carries
|
|
130
|
+
# the claim, including negation, conditions and numeric values.
|
|
131
|
+
key += (" ".join(atom.explanation.casefold().split()),)
|
|
132
|
+
current = merged.get(key)
|
|
133
|
+
if current is None:
|
|
134
|
+
merged[key] = atom
|
|
135
|
+
continue
|
|
136
|
+
merged[key] = current.model_copy(
|
|
137
|
+
update={
|
|
138
|
+
"source_ids": _ordered_union(current.source_ids, atom.source_ids),
|
|
139
|
+
"evidence_ids": _ordered_union(current.evidence_ids, atom.evidence_ids),
|
|
140
|
+
"topic_path": _ordered_union(current.topic_path, atom.topic_path),
|
|
141
|
+
# max of the *individual* judgments; not count-boosted
|
|
142
|
+
"confidence": max(current.confidence, atom.confidence),
|
|
143
|
+
"metadata": {**atom.metadata, **current.metadata},
|
|
144
|
+
}
|
|
145
|
+
)
|
|
146
|
+
return list(merged.values())
|
|
147
|
+
|
|
148
|
+
def merge(self, atoms: list[KnowledgeAtom]) -> list[KnowledgeAtom]:
|
|
149
|
+
merged: dict[tuple[str, ...], KnowledgeAtom] = {}
|
|
150
|
+
for atom in atoms:
|
|
151
|
+
key: tuple[str, ...] = (atom.atom_type, _normalize_content(atom.content))
|
|
152
|
+
if atom.atom_type == "core_concept":
|
|
153
|
+
key += (" ".join(atom.explanation.casefold().split()),)
|
|
154
|
+
if key not in merged:
|
|
155
|
+
merged[key] = atom
|
|
156
|
+
continue
|
|
157
|
+
|
|
158
|
+
current = merged[key]
|
|
159
|
+
merged[key] = current.model_copy(
|
|
160
|
+
update={
|
|
161
|
+
"timestamps": sorted(set(current.timestamps + atom.timestamps)),
|
|
162
|
+
"evidence": current.evidence + atom.evidence,
|
|
163
|
+
"confidence": max(current.confidence, atom.confidence),
|
|
164
|
+
}
|
|
165
|
+
)
|
|
166
|
+
return list(merged.values())
|