Repository: VOICEVOX/kanalizer
Pull request #70: feat!(infer): 推論が終了しなかったときにエラーを吐く
Author: login: sevenc-nanashi
Created: 2025-04-08T13:53:10Z | Merged: 2025-04-12T15:36:43Z

## 内容
タイトル通りです。

## 関連 Issue

（なし）

## スクリーンショット・動画など

（なし）

## その他

（なし）


Diff:
diff --git a/infer/crates/kanalizer-py/pyproject.toml b/infer/crates/kanalizer-py/pyproject.toml
index 044c4aae..47bc8bd3 100644
--- a/infer/crates/kanalizer-py/pyproject.toml
+++ b/infer/crates/kanalizer-py/pyproject.toml
@@ -18,3 +18,7 @@ dependencies = []
 
 [dependency-groups]
 dev = ["maturin>=1.8.2", "pyright>=1.1.398", "pytest>=8.3.5", "ruff>=0.11.0"]
+
+[tool.maturin]
+module-name = "kanalizer._rust"
+python-source = "python"
diff --git a/infer/crates/kanalizer-py/python/kanalizer/__init__.py b/infer/crates/kanalizer-py/python/kanalizer/__init__.py
new file mode 100644
index 00000000..c15a449c
--- /dev/null
+++ b/infer/crates/kanalizer-py/python/kanalizer/__init__.py
@@ -0,0 +1,15 @@
+from typing import TYPE_CHECKING
+from ._rust import __version__, convert, INPUT_CHARS, OUTPUT_CHARS
+from ._error import IncompleteConversionError
+
+if TYPE_CHECKING:
+    from ._rust import Strategy
+
+__all__ = [
+    "__version__",
+    "INPUT_CHARS",
+    "OUTPUT_CHARS",
+    "Strategy",
+    "convert",
+    "IncompleteConversionError",
+]
diff --git a/infer/crates/kanalizer-py/python/kanalizer/_error.py b/infer/crates/kanalizer-py/python/kanalizer/_error.py
new file mode 100644
index 00000000..ee2a14d0
--- /dev/null
+++ b/infer/crates/kanalizer-py/python/kanalizer/_error.py
@@ -0,0 +1,15 @@
+class IncompleteConversionError(Exception):
+    """
+    変換が終了しなかった場合に発生するエラー。
+
+    Attributes
+    ----------
+    incomplete_output : str
+        不完全な出力。
+    """
+
+    incomplete_output: str
+
+    def __init__(self, message: str, incomplete_output: str):
+        super().__init__(message)
+        self.incomplete_output = incomplete_output
diff --git a/infer/crates/kanalizer-py/kanalizer.pyi b/infer/crates/kanalizer-py/python/kanalizer/_rust.pyi
similarity index 75%
rename from infer/crates/kanalizer-py/kanalizer.pyi
rename to infer/crates/kanalizer-py/python/kanalizer/_rust.pyi
index e43e57de..16b947d2 100644
--- a/infer/crates/kanalizer-py/kanalizer.pyi
+++ b/infer/crates/kanalizer-py/python/kanalizer/_rust.pyi
@@ -17,6 +17,8 @@ def convert(
     /,
     *,
     max_length: int = 32,
+    strict: bool = True,
+    error_on_incomplete: bool = True,
     strategy: Literal["greedy"] = "greedy",
 ) -> str: ...
 @overload
@@ -25,6 +27,8 @@ def convert(
     /,
     *,
     max_length: int = 32,
+    strict: bool = True,
+    error_on_incomplete: bool = True,
     strategy: Literal["top_k"],
     k: int = 10,
 ) -> str: ...
@@ -34,12 +38,21 @@ def convert(
     /,
     *,
     max_length: int = 32,
+    strict: bool = True,
+    error_on_incomplete: bool = True,
     strategy: Literal["top_p"],
     p: float = 0.9,
     t: float = 1.0,
 ) -> str: ...
 def convert(
-    word: str, /, *, max_length: int = 32, strategy: Strategy = "greedy", **kwargs
+    word: str,
+    /,
+    *,
+    max_length: int = 32,
+    strategy: Strategy = "greedy",
+    strict: bool = True,
+    error_on_incomplete: bool = True,
+    **kwargs,
 ) -> str:
     """
     推論を行う。
@@ -53,6 +66,8 @@ def convert(
     strict : bool, default True
         入力の検証を行うかどうか。
         Falseの場合、無効な文字は無視されます。
+    error_on_incomplete : bool, default True
+        推論が終了しなかった場合にエラーを返すかどうか。
     strategy : Strategy, default "greedy"
         デコードのアルゴリズム。
     k : int, default 10
@@ -68,5 +83,7 @@ def convert(
         - strictがTrue、かつ`word`が空文字列の場合。
         - strictがTrue、かつ`word`にKanalizerの入力に使えない文字が含まれている場合。
         - `max_length`が0以下の場合。
+    IncompleteConversionError
+        - `error_on_incomplete`がTrue、かつ推論が終了しなかった場合。
     """
     ...
diff --git a/infer/crates/kanalizer-py/python/kanalizer/py.typed b/infer/crates/kanalizer-py/python/kanalizer/py.typed
new file mode 100644
index 00000000..e69de29b
diff --git a/infer/crates/kanalizer-py/src/lib.rs b/infer/crates/kanalizer-py/src/lib.rs
index d4d2b509..3977fdd4 100644
--- a/infer/crates/kanalizer-py/src/lib.rs
+++ b/infer/crates/kanalizer-py/src/lib.rs
@@ -87,11 +87,15 @@ fn extract_strategy(
     }
 }
 
+pyo3::import_exception!(kanalizer._error, IncompleteConversionError);
+
 #[pyfunction]
-#[pyo3(signature = (word, /, *, max_length = 32, strategy = "greedy", **kwargs))]
+#[pyo3(signature = (word, /, *, max_length = 32, strict = true, error_on_incomplete = true, strategy = "greedy", **kwargs))]
 fn convert(
     word: &str,
     max_length: usize,
+    strict: bool,
+    error_on_incomplete: bool,
     strategy: &str,
     kwargs: Option<&Bound<'_, PyDict>>,
 ) -> PyResult<String> {
@@ -101,6 +105,8 @@ fn convert(
             pyo3::exceptions::PyValueError::new_err("max_length must be a positive integer")
         })?)
         .with_strategy(&strategy)
+        .with_strict(strict)
+        .with_error_on_incomplete(error_on_incomplete)
         .perform();
 
     match result {
@@ -108,10 +114,18 @@ fn convert(
         Err(err @ (kanalizer::Error::EmptyInput | kanalizer::Error::InvalidChars { .. })) => {
             Err(pyo3::exceptions::PyValueError::new_err(err.to_string()))
         }
+        Err(
+            ref err @ kanalizer::Error::IncompleteConversion {
+                ref incomplete_output,
+            },
+        ) => Err(IncompleteConversionError::new_err((
+            err.to_string(),
+            incomplete_output.to_owned(),
+        ))),
     }
 }
 
-#[pymodule(name = "kanalizer")]
+#[pymodule(name = "_rust")]
 fn init_kanalizer(m: &Bound<'_, PyModule>) -> PyResult<()> {
     m.add("__version__", env!("CARGO_PKG_VERSION"))?;
     m.add("INPUT_CHARS", &*kanalizer::INPUT_CHARS)?;
diff --git a/infer/crates/kanalizer-py/test_main.py b/infer/crates/kanalizer-py/test_main.py
index 2995a29d..3d302753 100644
--- a/infer/crates/kanalizer-py/test_main.py
+++ b/infer/crates/kanalizer-py/test_main.py
@@ -27,3 +27,12 @@ def test_empty_word():
 def test_invalid_chars(word: str):
     with pytest.raises(ValueError):
         kanalizer.convert(word)
+
+
+def test_inference_not_finished_error():
+    word = "phosphoribosylaminoimidazolesuccinocarboxamide"
+    with pytest.raises(
+        kanalizer.IncompleteConversionError,
+        match=r'変換が終了しませんでした：".+"',
+    ):
+        kanalizer.convert(word, max_length=5)
diff --git a/infer/crates/kanalizer-rs/src/error.rs b/infer/crates/kanalizer-rs/src/error.rs
index ed2652a8..01cdb382 100644
--- a/infer/crates/kanalizer-rs/src/error.rs
+++ b/infer/crates/kanalizer-rs/src/error.rs
@@ -6,6 +6,8 @@ pub enum Error {
     EmptyInput,
     #[error("無効な文字が含まれています：{chars:?}")]
     InvalidChars { chars: Vec<char> },
+    #[error("変換が終了しませんでした：{incomplete_output:?}")]
+    IncompleteConversion { incomplete_output: String },
 }
 
 pub type Result<T> = std::result::Result<T, Error>;
diff --git a/infer/crates/kanalizer-rs/src/inference.rs b/infer/crates/kanalizer-rs/src/inference.rs
index f3ab4295..e449f900 100644
--- a/infer/crates/kanalizer-rs/src/inference.rs
+++ b/infer/crates/kanalizer-rs/src/inference.rs
@@ -17,6 +17,8 @@ pub struct ConvertOptions {
     /// 入力を検証する。
     /// falseの場合、無効な文字は無視されます。
     pub strict: bool,
+    /// 推論が終了しなかった場合にエラーを返す。
+    pub error_on_incomplete: bool,
 }
 
 impl Default for ConvertOptions {
@@ -25,6 +27,7 @@ impl Default for ConvertOptions {
             max_length: 32.try_into().unwrap(),
             strategy: Strategy::default(),
             strict: true,
+            error_on_incomplete: true,
         }
     }
 }
@@ -242,7 +245,7 @@ impl S2s {
         &self,
         source: &ndarray::Array1<usize>,
         options: &ConvertOptions,
-    ) -> ndarray::Array1<usize> {
+    ) -> E2kOutput<usize> {
         let e_emb = self.e_emb.forward(source);
         let (enc_out, _) = self.encoder.forward(&e_emb.view(), None);
         let (enc_out_rev, _) = self.encoder_reverse.forward(&e_emb.view(), None);
@@ -286,7 +289,11 @@ impl S2s {
             }
         }
 
-        ndarray::Array1::from(result)
+        let finished = result.last().unwrap() == &constants::EOS_IDX;
+        E2kOutput {
+            output: result,
+            finished,
+        }
     }
 }
 
@@ -296,6 +303,11 @@ struct BaseE2k<I: Hash + Eq, O: Clone> {
     out_table: HashMap<usize, O>,
 }
 
+struct E2kOutput<O> {
+    output: Vec<O>,
+    finished: bool,
+}
+
 impl<I: Hash + Eq, O: Clone> BaseE2k<I, O> {
     fn new(
         tensors: safetensors::SafeTensors,
@@ -308,26 +320,33 @@ impl<I: Hash + Eq, O: Clone> BaseE2k<I, O> {
             out_table,
         }
     }
-    fn infer(&self, input: &[I], options: &ConvertOptions) -> Vec<O> {
+    fn infer(&self, input: &[I], options: &ConvertOptions) -> E2kOutput<O> {
         let source = input
             .iter()
             .filter_map(|c| self.in_table.get(c).copied())
             .collect_vec();
         if source.is_empty() {
-            return Vec::new();
+            return E2kOutput {
+                output: vec![],
+                finished: true,
+            };
         }
         let source = [constants::SOS_IDX]
             .into_iter()
             .chain(source)
             .chain([constants::EOS_IDX]);
         let source = ndarray::Array1::from_iter(source);
-        let target = self.s2s.forward(&source, options);
-        target
-            .iter()
-            .skip(1)
-            .take_while(|&&x| x != constants::EOS_IDX)
-            .map(|&x| self.out_table[&x].clone())
-            .collect()
+        let result = self.s2s.forward(&source, options);
+        E2kOutput {
+            output: result
+                .output
+                .iter()
+                .skip(1)
+                .take_while(|&&x| x != constants::EOS_IDX)
+                .map(|&x| self.out_table[&x].clone())
+                .collect(),
+            finished: result.finished,
+        }
     }
 }
 
@@ -406,7 +425,15 @@ impl Kanalizer {
             self.validate_input(input)?;
         }
         let input = input.chars().map(|c| c.to_string()).collect::<Vec<_>>();
-        Ok(self.inner.infer(&input, options).into_iter().collect())
+        let infer_result = self.inner.infer(&input, options);
+        let output = infer_result.output.iter().collect();
+        if !infer_result.finished && options.error_on_incomplete {
+            return Err(Error::IncompleteConversion {
+                incomplete_output: output,
+            });
+        }
+
+        Ok(output)
     }
 
     fn validate_input(&self, input: &str) -> Result<()> {
diff --git a/infer/crates/kanalizer-rs/src/lib.rs b/infer/crates/kanalizer-rs/src/lib.rs
index 9eefab92..078b5661 100644
--- a/infer/crates/kanalizer-rs/src/lib.rs
+++ b/infer/crates/kanalizer-rs/src/lib.rs
@@ -69,6 +69,12 @@ impl ConvertBuilder {
         self
     }
 
+    /// 推論が終了しなかった場合にエラーを返すかどうかを指定する。
+    pub fn with_error_on_incomplete(mut self, error_on_incomplete: bool) -> Self {
+        self.options.error_on_incomplete = error_on_incomplete;
+        self
+    }
+
     /// 推論を行う。
     pub fn perform(self) -> Result<String> {
         KANALIZER.convert(&self.word, &self.options)
diff --git a/infer/crates/kanalizer-rs/tests/test_main.rs b/infer/crates/kanalizer-rs/tests/test_main.rs
index c59a0a19..e81a26b9 100644
--- a/infer/crates/kanalizer-rs/tests/test_main.rs
+++ b/infer/crates/kanalizer-rs/tests/test_main.rs
@@ -26,6 +26,7 @@ fn test_kanalizer_long() {
             src,
             &kanalizer::ConvertOptions {
                 max_length: 10.try_into().unwrap(),
+                error_on_incomplete: false,
                 ..Default::default()
             },
         )
@@ -34,6 +35,27 @@ fn test_kanalizer_long() {
     assert_eq!(limited_dst.chars().count(), 10);
 }
 
+#[test]
+fn test_kanalizer_long_error() {
+    let src = "phosphoribosylaminoimidazolesuccinocarboxamide";
+
+    let kanalizer = kanalizer::Kanalizer::new();
+    let limited_dst = kanalizer
+        .convert(
+            src,
+            &kanalizer::ConvertOptions {
+                max_length: 10.try_into().unwrap(),
+                error_on_incomplete: true,
+                ..Default::default()
+            },
+        )
+        .unwrap_err();
+    assert!(matches!(
+        limited_dst,
+        kanalizer::Error::IncompleteConversion { .. }
+    ));
+}
+
 #[test]
 fn test_validate_empty() {
     let src = "";


Reviews:
user: login: qryxip
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-10T15:31:52Z
user: login: sevenc-nanashi
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-10T15:46:13Z
user: login: Hiroshiba
body: 全体的に良さそうな雰囲気！
「推論」という言葉がAPIとして外に出てるのが気になるかなくらい。

ちょっとAI君に「あえてコメントしてもらうなら」でコメントもらいました！
良さそうなポイントあったら適用しても良いかも？

* Rustのエラーメッセージに不完全な出力も含めた方がいいのだ！
* テストケースをもっと充実させて、エラーから取得した不完全な出力の検証もした方がいい！
* 「incomplete」と「not finished」の用語を統一した方がコードが読みやすくなるのだ！
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-11T06:29:54Z
user: login: Hiroshiba
body: あ。
ちょっと考えきれてないので提案というか話題に上げてみるレベルなのですが、`strict`と`error_on_incomplete`をうまいこと混ぜられたりしないですかねぇ。

こう、全ての想定外に対してエラーを出す・英単語が入力されてればとりあえずエラーにはならない・どんな文字列でもエラーにはならない、warningすら出さないみたいな･･･？

まあでもこのPRとしてはこの形が良さそう！
書くならタスクリストかも。
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-11T06:31:43Z
user: login: sevenc-nanashi
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-11T14:06:16Z
user: login: qryxip
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-11T19:38:19Z
user: login: qryxip
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-11T19:44:00Z
user: login: Hiroshiba
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-12T00:07:02Z
user: login: qryxip
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-12T02:37:55Z
user: login: qryxip
body: LGTM!
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-12T02:46:52Z
user: login: qryxip
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-12T02:55:36Z
user: login: Hiroshiba
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-12T02:58:39Z
user: login: qryxip
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-12T03:05:39Z
user: login: Hiroshiba
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-12T04:44:57Z
user: login: copilot-pull-request-reviewer[bot]
body: 

Copilot reviewed 11 out of 11 changed files in this pull request and generated no comments.




state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-12T04:46:47Z
user: login: Hiroshiba
body: LGTM！！
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-12T04:50:27Z
user: login: sevenc-nanashi
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-12T13:59:01Z
user: login: Hiroshiba
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-12T14:56:15Z

Review comments:
diff_hunk: @@ -296,6 +303,11 @@ struct BaseE2k<I: Hash + Eq, O: Clone> {
     out_table: HashMap<usize, O>,
 }
 
+struct E2kResult<O> {
path: infer/crates/kanalizer-rs/src/inference.rs
user: login: qryxip
body: Rustにおいては若干混乱をまねく名前かなと。私は"output"とか"outcome"とかをよく使っています。
created_at: 2025-04-10T15:31:52Z
author_association: MEMBER
diff_hunk: @@ -296,6 +303,11 @@ struct BaseE2k<I: Hash + Eq, O: Clone> {
     out_table: HashMap<usize, O>,
 }
 
+struct E2kResult<O> {
path: infer/crates/kanalizer-rs/src/inference.rs
user: login: sevenc-nanashi
body: Outputにします。
created_at: 2025-04-10T15:46:13Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,15 @@
+class InferenceNotFinishedError(Exception):
+    """
+    推論が終了しなかった場合に発生するエラー。
path: infer/crates/kanalizer-py/python/kanalizer/_error.py
user: login: Hiroshiba
body: 「推論」という言葉が露出するのはかなり避けたほうが良さそうな気持ち。
ユーザーの気持ち的にやっているのは推論ではなく変換or文字生成なはず。

`ConversionIncompleteError`とか？
`ReadingGenerationIncompleteError`とか･･･？

「生成した文字が長過ぎるエラー」みたいな具体的なのでも良いかも。これ以外エラーないだろうし。
created_at: 2025-04-11T05:24:43Z
author_association: MEMBER
diff_hunk: @@ -87,11 +87,15 @@ fn extract_strategy(
     }
 }
 
+pyo3::import_exception!(kanalizer._error, InferenceNotFinishedError);
+
 #[pyfunction]
-#[pyo3(signature = (word, /, *, max_length = 32, strategy = "greedy", **kwargs))]
+#[pyo3(signature = (word, /, *, max_length = 32, strict = true, error_on_incomplete = true, strategy = "greedy", **kwargs))]
 fn convert(
     word: &str,
     max_length: usize,
+    strict: bool,
+    error_on_incomplete: bool,
path: infer/crates/kanalizer-py/src/lib.rs
user: login: Hiroshiba
body: この２つ、定義の順番がいろんなとこで逆なの気になりました！
created_at: 2025-04-11T06:22:56Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,15 @@
+class InferenceNotFinishedError(Exception):
+    """
+    推論が終了しなかった場合に発生するエラー。
path: infer/crates/kanalizer-py/python/kanalizer/_error.py
user: login: sevenc-nanashi
body: IncompleteConversionにしました。
created_at: 2025-04-11T14:06:16Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,15 @@
+class InferenceNotFinishedError(Exception):
+    """
+    推論が終了しなかった場合に発生するエラー。
path: infer/crates/kanalizer-py/python/kanalizer/_error.py
user: login: qryxip
body: 前から思っていたのですが、kanalizerを使うユーザー層ってどんな感じになるのでしょうか？少なくともCORE/ENGINEを使う層よりは、平均的にプログラミングに慣れている人達が使うのではないかと想像しています。もしそうであるなら正直"推論"/"infer"という言葉は一般的なのでそのまま通じるし、性質を正確に表現している言葉なのでそのままでいいじゃないか…と思ったのですが、こういう話って前議論してたりしてましたっけ？
(「推論という言葉を外に出さない」という話自体は何度かされていたと思うのですが、その是非については深く議論したことはなかったような気がしています)
created_at: 2025-04-11T19:38:19Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,15 @@
+class InferenceNotFinishedError(Exception):
+    """
+    推論が終了しなかった場合に発生するエラー。
path: infer/crates/kanalizer-py/python/kanalizer/_error.py
user: login: qryxip
body: というのもこのPRのタイトルも「推論」ですし、コード中にもなんか「推論」という単語が残ってそうな状態ではあるので、"変換"/"convert"の方に倒すなら倒すである程度はっきりとした意志で決めた方がよさげな気がした次第です。
created_at: 2025-04-11T19:44:00Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,15 @@
+class InferenceNotFinishedError(Exception):
+    """
+    推論が終了しなかった場合に発生するエラー。
path: infer/crates/kanalizer-py/python/kanalizer/_error.py
user: login: Hiroshiba
body: うーん　infer使うかはちょっと長くなりそうなのでissue建てて議論したいですね

ユーザー層はだいぶライト寄りかなと思ってます。
COREと同程度か少し簡単寄り、pyopenjtalkよりだいぶ簡単くらいのイメージ。
created_at: 2025-04-12T00:07:02Z
author_association: MEMBER
diff_hunk: @@ -406,7 +425,19 @@ impl Kanalizer {
             self.validate_input(input)?;
         }
         let input = input.chars().map(|c| c.to_string()).collect::<Vec<_>>();
-        Ok(self.inner.infer(&input, options).into_iter().collect())
+        let infer_result = self.inner.infer(&input, options);
+        let output = infer_result
+            .output
+            .iter()
+            .map(|c| c.to_string())
+            .collect::<String>();
path: infer/crates/kanalizer-rs/src/inference.rs
user: login: qryxip
body: [`impl<'a> FromIterator<&'a char> for String`](https://doc.rust-lang.org/stable/std/string/struct.String.html#impl-FromIterator%3C%26char%3E-for-String)

```suggestion
        let output = infer_result.output.iter().collect();
```
created_at: 2025-04-12T02:36:44Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,15 @@
+class InferenceNotFinishedError(Exception):
+    """
+    推論が終了しなかった場合に発生するエラー。
path: infer/crates/kanalizer-py/python/kanalizer/_error.py
user: login: qryxip
body: 雑に作ってみました: #77
created_at: 2025-04-12T02:55:35Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,15 @@
+class InferenceNotFinishedError(Exception):
+    """
+    推論が終了しなかった場合に発生するエラー。
path: infer/crates/kanalizer-py/python/kanalizer/_error.py
user: login: Hiroshiba
body: うおおありがとうございます！！！　コメントが引用されていて丁寧･･･！！！

一旦ここはresolveで良さそう･･･！　resolve！
created_at: 2025-04-12T02:58:39Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,15 @@
+class InferenceNotFinishedError(Exception):
+    """
+    推論が終了しなかった場合に発生するエラー。
path: infer/crates/kanalizer-py/python/kanalizer/_error.py
user: login: qryxip
body: Reference in new issueで作られるやつを活用してみました。引用部分がよくわからない形式で出てくるので、そこだけ`> ` (`<blockquote>`)にする形で。

![image](https://github.com/user-attachments/assets/ffccca25-937c-4009-ae89-e97f351c1c09)
![image](https://github.com/user-attachments/assets/5a8b24ea-8e34-46c8-a280-aeedcf213b5d)

まとめなおすのに頭をあまり使わなくてよいのでこれ結構よいかも？
created_at: 2025-04-12T03:05:39Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,15 @@
+class InferenceNotFinishedError(Exception):
+    """
+    推論が終了しなかった場合に発生するエラー。
path: infer/crates/kanalizer-py/python/kanalizer/_error.py
user: login: Hiroshiba
body: へー！！　`Reference in new issue`ってのがあるんですね、便利だ。

created_at: 2025-04-12T04:44:57Z
author_association: MEMBER
diff_hunk: @@ -296,6 +303,11 @@ struct BaseE2k<I: Hash + Eq, O: Clone> {
     out_table: HashMap<usize, O>,
 }
 
+struct E2kOutput<O> {
path: infer/crates/kanalizer-rs/src/inference.rs
user: login: Hiroshiba
body: （ただのコメントです）

そういえばまだ`e2k`という言葉がちらほら残ってるかも？
created_at: 2025-04-12T04:48:55Z
author_association: MEMBER
diff_hunk: @@ -296,6 +303,11 @@ struct BaseE2k<I: Hash + Eq, O: Clone> {
     out_table: HashMap<usize, O>,
 }
 
+struct E2kOutput<O> {
path: infer/crates/kanalizer-rs/src/inference.rs
user: login: sevenc-nanashi
body: これは本家e2kと合わせたいからですね（BaseE2kと揃えてる感じ）
created_at: 2025-04-12T13:59:00Z
author_association: MEMBER
diff_hunk: @@ -296,6 +303,11 @@ struct BaseE2k<I: Hash + Eq, O: Clone> {
     out_table: HashMap<usize, O>,
 }
 
+struct E2kOutput<O> {
path: infer/crates/kanalizer-rs/src/inference.rs
user: login: Hiroshiba
body: あぁなるほど。。
もっと別の名前にしてもいいかもしれない･･･？

とりあえずresolve！
created_at: 2025-04-12T14:56:15Z
author_association: MEMBER

PR comments:
user: login: sevenc-nanashi
created_at: 2025-04-10T14:01:12Z
body: 結構な大工事になっちゃいました...（Rust only -> [Mixed Rust/Python project](https://www.maturin.rs/project_layout.html#mixed-rustpython-project)の工事が含まれる）
author_association: MEMBER
user: login: sevenc-nanashi
created_at: 2025-04-12T15:34:32Z
body: マージします。
author_association: MEMBER