|
| 1 | +# from __future__ import annotations |
| 2 | + |
| 3 | +# import json |
| 4 | +# import re |
| 5 | +# from dataclasses import dataclass |
| 6 | +# from pathlib import Path |
| 7 | +# from typing import Dict, List, Optional, Pattern, Tuple |
| 8 | + |
| 9 | +# import numpy as np |
| 10 | + |
| 11 | +# from back.app.resources.template_encoder import TemplateEncoder |
| 12 | + |
| 13 | + |
| 14 | +# @dataclass(frozen=True) |
| 15 | +# class TemplateEntry: |
| 16 | +# """ |
| 17 | +# テンプレートリソースに保存される1件分のエントリ。 |
| 18 | +# """ |
| 19 | + |
| 20 | +# template_id: str |
| 21 | +# theme: str |
| 22 | +# code: str |
| 23 | + |
| 24 | + |
| 25 | +# class TemplateRetriever: |
| 26 | +# """ |
| 27 | +# テンプレート情報の読み込みと埋め込み検索を担当するクラス。 |
| 28 | +# """ |
| 29 | + |
| 30 | +# MODEL_NAME = "cl-nagoya/ruri-v3-30m" |
| 31 | + |
| 32 | +# def __init__(self, resource_dir: Path) -> None: |
| 33 | +# """ |
| 34 | +# テンプレート情報と埋め込みを読み込み、埋め込みモデルを初期化する。 |
| 35 | +# """ |
| 36 | +# self.resource_dir = resource_dir |
| 37 | +# self.resource_dir.mkdir(parents=True, exist_ok=True) |
| 38 | +# self.embeddings_path = self.resource_dir / "theme_embeddings.npy" |
| 39 | +# self.templates_path = self.resource_dir / "manim_template.jsonl" |
| 40 | + |
| 41 | +# self.templates: List[TemplateEntry] = self._load_templates() |
| 42 | +# self.embeddings: np.ndarray = self._load_embeddings() |
| 43 | +# if self.embeddings.shape[0] != len(self.templates): |
| 44 | +# raise ValueError("Template count and embedding rows mismatch.") |
| 45 | + |
| 46 | +# self._template_by_id: Dict[str, TemplateEntry] = { |
| 47 | +# entry.template_id: entry for entry in self.templates |
| 48 | +# } |
| 49 | +# self._sequence_pattern_cache: Dict[str, Pattern[str]] = {} |
| 50 | + |
| 51 | +# self.encoder = TemplateEncoder(self.MODEL_NAME) |
| 52 | + |
| 53 | +# def _load_templates(self) -> List[TemplateEntry]: |
| 54 | +# """ |
| 55 | +# JSONL からテンプレート情報を読み込む。ファイルが無ければ空リストを返す。 |
| 56 | +# """ |
| 57 | +# entries: List[TemplateEntry] = [] |
| 58 | +# if not self.templates_path.exists(): |
| 59 | +# return entries |
| 60 | +# with self.templates_path.open("r", encoding="utf-8") as fh: |
| 61 | +# for line in fh: |
| 62 | +# payload = line.strip() |
| 63 | +# if not payload: |
| 64 | +# continue |
| 65 | +# record = json.loads(payload) |
| 66 | +# entries.append( |
| 67 | +# TemplateEntry( |
| 68 | +# template_id=record["id"], |
| 69 | +# theme=record["theme"], |
| 70 | +# code=record.get("code", ""), |
| 71 | +# ) |
| 72 | +# ) |
| 73 | +# return entries |
| 74 | + |
| 75 | +# def _load_embeddings(self) -> np.ndarray: |
| 76 | +# """ |
| 77 | +# 埋め込み行列を取得し、各行が単位ベクトルになるよう正規化する。 |
| 78 | +# """ |
| 79 | +# if not self.embeddings_path.exists(): |
| 80 | +# return np.empty((0, 0), dtype=np.float32) |
| 81 | +# matrix = np.load(self.embeddings_path) |
| 82 | +# if matrix.size == 0: |
| 83 | +# return matrix |
| 84 | +# norms = np.linalg.norm(matrix, axis=1, keepdims=True) |
| 85 | +# norms[norms == 0] = 1e-9 |
| 86 | +# return matrix / norms |
| 87 | + |
| 88 | +# def encode(self, text: str, *, max_length: int = 256) -> np.ndarray: |
| 89 | +# """ |
| 90 | +# テキストをエンコードし、正規化済みの文ベクトルを返す。 |
| 91 | +# """ |
| 92 | +# return self.encoder.encode(text, max_length=max_length) |
| 93 | + |
| 94 | +# def allocate_template_id( |
| 95 | +# self, template_id: Optional[str] = None, *, prefix: str = "code" |
| 96 | +# ) -> str: |
| 97 | +# """ |
| 98 | +# 新しいテンプレートIDを取得する。テンプレートID指定時は重複チェックのみ行う。 |
| 99 | +# """ |
| 100 | +# if template_id: |
| 101 | +# if template_id in self._template_by_id: |
| 102 | +# raise ValueError("指定された template_id は既に存在します。") |
| 103 | +# return template_id |
| 104 | +# next_seq = self._compute_next_sequence(prefix) |
| 105 | +# return f"{prefix}{next_seq}" |
| 106 | + |
| 107 | +# def append_entry( |
| 108 | +# self, entry: TemplateEntry, embedding: np.ndarray, *, persist: bool = False |
| 109 | +# ) -> None: |
| 110 | +# """ |
| 111 | +# 新しいテンプレートと埋め込みを登録し、必要に応じて永続化する。 |
| 112 | +# """ |
| 113 | +# if embedding.ndim != 1: |
| 114 | +# raise ValueError("Embedding must be a 1-D vector.") |
| 115 | +# norm = np.linalg.norm(embedding) |
| 116 | +# if norm == 0: |
| 117 | +# raise ValueError("Embedding norm is zero.") |
| 118 | +# normalized = (embedding / norm).astype(np.float32) |
| 119 | + |
| 120 | +# if entry.template_id in self._template_by_id: |
| 121 | +# raise ValueError( |
| 122 | +# f"template_id '{entry.template_id}' is already registered." |
| 123 | +# ) |
| 124 | + |
| 125 | +# if self.embeddings.size == 0: |
| 126 | +# self.embeddings = normalized[None, :] |
| 127 | +# else: |
| 128 | +# self.embeddings = np.vstack([self.embeddings, normalized]) |
| 129 | + |
| 130 | +# self.templates.append(entry) |
| 131 | +# self._template_by_id[entry.template_id] = entry |
| 132 | + |
| 133 | +# if persist: |
| 134 | +# self._append_template_to_disk(entry) |
| 135 | +# self._save_embeddings() |
| 136 | + |
| 137 | +# def get_by_id(self, template_id: str) -> Optional[TemplateEntry]: |
| 138 | +# return self._template_by_id.get(template_id) |
| 139 | + |
| 140 | +# def topk(self, query: np.ndarray, *, k: int = 3) -> List[Tuple[int, float]]: |
| 141 | +# """ |
| 142 | +# 類似度の高い順に上位 k 件の (インデックス, スコア) を返す。 |
| 143 | +# """ |
| 144 | +# if self.embeddings.size == 0: |
| 145 | +# return [] |
| 146 | +# scores = self.embeddings @ query |
| 147 | +# ranked = np.argsort(scores)[::-1] |
| 148 | +# return [(int(idx), float(scores[idx])) for idx in ranked[:k]] |
| 149 | + |
| 150 | +# def remove_latest(self, *, persist: bool = False) -> TemplateEntry: |
| 151 | +# """ |
| 152 | +# 最後に追加されたテンプレートを削除し、必要に応じて永続化する。 |
| 153 | +# """ |
| 154 | +# if not self.templates: |
| 155 | +# raise ValueError("テンプレートが登録されていません。") |
| 156 | + |
| 157 | +# removed_entry = self.templates.pop() |
| 158 | +# self._template_by_id.pop(removed_entry.template_id, None) |
| 159 | + |
| 160 | +# if self.embeddings.size == 0 or self.embeddings.shape[0] <= 1: |
| 161 | +# self.embeddings = np.empty((0, 0), dtype=np.float32) |
| 162 | +# else: |
| 163 | +# self.embeddings = self.embeddings[:-1, :] |
| 164 | + |
| 165 | +# if persist: |
| 166 | +# self._write_templates_file() |
| 167 | +# self._save_embeddings() |
| 168 | + |
| 169 | +# return removed_entry |
| 170 | + |
| 171 | +# def _compute_next_sequence(self, prefix: str) -> int: |
| 172 | +# pattern = self._get_id_pattern(prefix) |
| 173 | +# max_seq = 0 |
| 174 | +# for entry in self.templates: |
| 175 | +# match = pattern.match(entry.template_id) |
| 176 | +# if match: |
| 177 | +# max_seq = max(max_seq, int(match.group(1))) |
| 178 | +# return max_seq + 1 |
| 179 | + |
| 180 | +# def _append_template_to_disk(self, entry: TemplateEntry) -> None: |
| 181 | +# payload = { |
| 182 | +# "id": entry.template_id, |
| 183 | +# "theme": entry.theme, |
| 184 | +# "code": entry.code, |
| 185 | +# } |
| 186 | +# with self.templates_path.open("a", encoding="utf-8") as fh: |
| 187 | +# fh.write(json.dumps(payload, ensure_ascii=False)) |
| 188 | +# fh.write("\n") |
| 189 | + |
| 190 | +# def _write_templates_file(self) -> None: |
| 191 | +# if not self.templates: |
| 192 | +# self.templates_path.write_text("", encoding="utf-8") |
| 193 | +# return |
| 194 | + |
| 195 | +# lines = [] |
| 196 | +# for entry in self.templates: |
| 197 | +# payload = { |
| 198 | +# "id": entry.template_id, |
| 199 | +# "theme": entry.theme, |
| 200 | +# "code": entry.code, |
| 201 | +# } |
| 202 | +# lines.append(json.dumps(payload, ensure_ascii=False)) |
| 203 | +# self.templates_path.write_text("\n".join(lines) + "\n", encoding="utf-8") |
| 204 | + |
| 205 | +# def _save_embeddings(self) -> None: |
| 206 | +# np.save(self.embeddings_path, self.embeddings) |
| 207 | + |
| 208 | +# def _get_id_pattern(self, prefix: str) -> Pattern[str]: |
| 209 | +# pattern = self._sequence_pattern_cache.get(prefix) |
| 210 | +# if pattern is None: |
| 211 | +# pattern = re.compile(rf"^{re.escape(prefix)}(\d+)$") |
| 212 | +# self._sequence_pattern_cache[prefix] = pattern |
| 213 | +# return pattern |
0 commit comments