siyuan-note/siyuan · error
invalid OCR metadata
Error message
invalid OCR metadata
What it means
PrepareAssetTextRelinks reads data/assets/ocr-texts.json and unmarshals it into a map[string]string. If the file exists but its content is not valid JSON, or parses to JSON null (which would leave p.texts nil), the function refuses to proceed with "invalid OCR metadata" rather than silently discarding or rebuilding OCR data.
Solutions
- Validate the file: run it through a JSON parser to find the syntax error at data/assets/ocr-texts.json.
- Fix the JSON syntax manually (it is a flat JSON object of asset-path → OCR-text string pairs).
- If unrecoverable, restore ocr-texts.json from history (historyDir/assets/ocr-texts.json backups) or from sync snapshots.
- Do NOT simply delete the file: only do so if losing OCR text is acceptable, as it makes the file 'not exist' and resets metadata.
- Re-run OCR (image OCR recognition) afterwards to rebuild entries if the file had to be recreated.
Example fix
// before: unmarshalling broken file
// data/assets/ocr-texts.json: {"assets/foo.png": "text",}
// after: repair to valid JSON object or restore from backup
// {"assets/foo.png": "text"} Defensive patterns
Strategy: validation
Validate before calling
data, err := os.ReadFile(filepath.Join(util.GetDataAssetsAbsPath(), "ocr-texts.json"))
if err == nil {
var probe map[string]string
if json.Unmarshal(data, &probe) != nil || probe == nil {
// restore from history backup before calling PrepareAssetTextRelinks
}
} Try / catch
plan, err := util.PrepareAssetTextRelinks(mappings)
if err != nil && err.Error() == "invalid OCR metadata" {
// restore data/assets/ocr-texts.json from historyDir/assets/ocr-texts.json, then retry
return restoreOCRBackupAndRetry(mappings)
} Prevention
- Never hand-edit ocr-texts.json without validating with a JSON parser afterwards
- Restore from historyDir/assets/ocr-texts.json backups after a crash
- Treat any truncation symptom (sync conflicts, disk-full writes) as a signal to validate the file
- Ensure the file is UTF-8 without BOM
When it happens
Trigger: Calling PrepareAssetTextRelinks (directly or via CopyAssetTextForRelink / scanMetadata) when data/assets/ocr-texts.json contains malformed JSON (truncated write, manual edit, encoding error) or literal `null`.
Common situations: A crash or power loss during a previous write left a truncated file; the user hand-edited ocr-texts.json and broke JSON syntax; the file was saved with a BOM or in a non-UTF-8 encoding; sync restored a partially written copy.
Understand the failure class
Background: "failed to unmarshal" / json.Unmarshal errors: why parsing a response into a Go struct fails and how to fix it — this error's family across 23 libraries.
Related errors
- block ID must be text
- block operation result must be text, block IDs or null
- invalid cloud login code
- invalid cloud login message
- parse encrypted notebook history conf
AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19).
Data as JSON: /api/errors/1185526d95f0ef0b.
Report an issue: GitHub.
Appendix: source
Thrown at kernel/util/ocr_relink_batch.go:55
type assetTextIndexedValue struct {
value string
conflict bool
}
// PrepareAssetTextRelinks 从同一份 OCR 快照计算所有映射,不写文件或内存缓存。
func PrepareAssetTextRelinks(mappings []AssetTextRelinkMapping) (*AssetTextRelinkPlan, error) {
assetsTextsLock.Lock()
defer assetsTextsLock.Unlock()
p := &AssetTextRelinkPlan{mappings: mappings, Results: make([]AssetTextRelinkResult, len(mappings)),
texts: map[string]string{}, memory: maps.Clone(assetsTexts), values: make([]string, len(mappings)),
path: filepath.Join(GetDataAssetsAbsPath(), "ocr-texts.json")}
data, err := filelock.ReadFile(p.path)
if err != nil && !os.IsNotExist(err) {
return nil, err
}
if err == nil {
if err = json.Unmarshal(data, &p.texts); err != nil || p.texts == nil {
return nil, errors.New("invalid OCR metadata")
}
p.original = data
}
maps.Copy(p.texts, p.memory)
index := map[string]assetTextIndexedValue{}
add := func(key, value string) {
if prior, found := index[key]; found {
prior.conflict = prior.conflict || prior.value != value
index[key] = prior
} else {
index[key] = assetTextIndexedValue{value: value}
}
}
for key, value := range p.texts {
add(key, value)
parsed, parseErr := url.Parse(key)
if parseErr == nil && !parsed.IsAbs() && parsed.Host == "" {
canonical := strings.TrimPrefix(parsed.Path, "/")View on GitHub (pinned to 9f775e8a12)