370 lines
16 KiB
Dart
370 lines
16 KiB
Dart
import 'dart:convert';
|
|
|
|
import '../models/culture_branch.dart';
|
|
import '../models/word_model.dart';
|
|
|
|
/// Роли ручного бурения. Человек сам отвечает вместо моделей.
|
|
class DiveRole {
|
|
final String id;
|
|
final String title;
|
|
final String hint;
|
|
|
|
const DiveRole(this.id, this.title, this.hint);
|
|
}
|
|
|
|
const List<DiveRole> kDiveRoles = [
|
|
DiveRole(
|
|
'route_cultures',
|
|
'Маршрут культур',
|
|
'Три традиции, где у знака есть именной след. Ответ — названия через запятую.',
|
|
),
|
|
DiveRole(
|
|
'route_languages',
|
|
'Маршрут языков',
|
|
'Три языка с прямым корнем или устойчивым именем образа. Ответ — названия через запятую.',
|
|
),
|
|
DiveRole(
|
|
'mystic',
|
|
'Мистик',
|
|
'Закон знака: устройство, свет, тень и какие системы держат именно это слово.',
|
|
),
|
|
DiveRole(
|
|
'psycho',
|
|
'Психоаналитик',
|
|
'Один психический механизм этого знака, не архетип вообще.',
|
|
),
|
|
DiveRole(
|
|
'synth',
|
|
'Синтезатор',
|
|
'Сжимает уже вписанные ответы в карточку. JSON можно разложить по полям.',
|
|
),
|
|
];
|
|
|
|
class DiveSlot {
|
|
final String id;
|
|
final String title;
|
|
|
|
const DiveSlot(this.id, this.title);
|
|
}
|
|
|
|
const List<DiveSlot> kDiveLanguages = [
|
|
DiveSlot('шумерский', 'шумерский'),
|
|
DiveSlot('древнеегипетский', 'древнеегипетский'),
|
|
DiveSlot('иврит', 'иврит'),
|
|
DiveSlot('древнегреческий', 'древнегреческий'),
|
|
DiveSlot('латынь', 'латынь'),
|
|
DiveSlot('древнеирландский', 'древнеирландский'),
|
|
DiveSlot('древнескандинавский', 'древнескандинавский'),
|
|
DiveSlot('старославянский', 'старославянский'),
|
|
DiveSlot('санскрит', 'санскрит'),
|
|
DiveSlot('авестийский', 'авестийский'),
|
|
DiveSlot('древнекитайский', 'древнекитайский'),
|
|
DiveSlot('японский', 'японский'),
|
|
DiveSlot('арабский', 'арабский'),
|
|
DiveSlot('майя', 'майя'),
|
|
DiveSlot('церковнославянский', 'церковнославянский'),
|
|
];
|
|
|
|
const List<DiveSlot> kDiveTraditions = [
|
|
DiveSlot('runes', 'Руны'),
|
|
DiveSlot('tarot', 'Таро'),
|
|
DiveSlot('ogham', 'Огам'),
|
|
DiveSlot('kabbalah', 'Каббала'),
|
|
DiveSlot('lenormand', 'Ленорман'),
|
|
DiveSlot('geomancy', 'Геомантия'),
|
|
DiveSlot('iching', 'И Цзин'),
|
|
DiveSlot('alchemy', 'Алхимия'),
|
|
DiveSlot('astrology', 'Астрология'),
|
|
DiveSlot('cosmology', 'Космология'),
|
|
DiveSlot('slavic', 'Славянский канон'),
|
|
];
|
|
|
|
const Map<String, String> _languageAliases = {
|
|
'греческий': 'древнегреческий',
|
|
'китайский': 'древнекитайский',
|
|
'египетский': 'древнеегипетский',
|
|
'скандинавский': 'древнескандинавский',
|
|
'ирландский': 'древнеирландский',
|
|
'славянский': 'старославянский',
|
|
'церковный славянский': 'церковнославянский',
|
|
};
|
|
|
|
String _dossier(WordItem word) {
|
|
final pos = word.partOfSpeech.trim().isEmpty ? 'не указана' : word.partOfSpeech.trim();
|
|
final category = word.category.trim().isEmpty ? 'не указана' : word.category.trim();
|
|
final buffer = StringBuffer()
|
|
..writeln('Знак Оракула 999: «${word.word}».')
|
|
..writeln('Часть речи: $pos. Категория: $category.');
|
|
if (word.core.trim().isNotEmpty) {
|
|
buffer.writeln('Текущее ядро, его можно уточнить, но не подменяй другим знаком: ${word.core.trim()}');
|
|
}
|
|
return buffer.toString().trimRight();
|
|
}
|
|
|
|
String cultureRoutePrompt(WordItem word) {
|
|
final menu = kCultureBranches.map((b) => b.name).join('; ');
|
|
return '${_dossier(word)}\n'
|
|
'Выбери ровно 3 традиции, где у ЭТОГО образа есть именной миф, текст или обряд, '
|
|
'а не просто «похожая идея».\n'
|
|
'Только из списка: $menu.\n'
|
|
'Ответ: три названия через запятую. Без пояснений.';
|
|
}
|
|
|
|
String languageRoutePrompt(WordItem word) {
|
|
final menu = kDiveLanguages.map((b) => b.title).join('; ');
|
|
return '${_dossier(word)}\n'
|
|
'Выбери ровно 3 языка, где есть прямой корень, устойчивый перевод этого образа '
|
|
'или его иероглиф. Не выбирай язык только потому, что он древний.\n'
|
|
'Только из списка: $menu.\n'
|
|
'Ответ: три названия через запятую. Без пояснений.';
|
|
}
|
|
|
|
String mysticPrompt(WordItem word) {
|
|
return 'Ты мистик Оракула 999. Разбираешь один знак как закон, не как гороскоп. '
|
|
'Пиши по-русски. Не предсказывай любовь, деньги и здоровье. '
|
|
'Не занимай чужую карту, руну или гексаграмму, если этот знак ею не является. '
|
|
'Если след тонок — так и скажи.\n\n'
|
|
'${_dossier(word)}\n'
|
|
'Дай: устройство образа; светлую сторону того же закона; теневую сторону того же закона; '
|
|
'какие системы (Таро, руны, Ленорман, геомантия, И Цзин, алхимия, астрология) '
|
|
'держат именно это слово, а какие только соседние. Соседние не присваивай.';
|
|
}
|
|
|
|
String psychoPrompt(WordItem word) {
|
|
return 'Ты юнгианский аналитик. Один знак — один психический механизм. '
|
|
'Пиши по-русски, без лекции про индивидуацию вообще. '
|
|
'Назови, что человек делает с этим образом, когда он интегрирован, '
|
|
'и что делает, когда вытесняет.\n\n'
|
|
'${_dossier(word)}\n'
|
|
'Разбери только его, не архетип «вообще».';
|
|
}
|
|
|
|
String cultureSlicePrompt(WordItem word, String culture) {
|
|
return 'Ты мифолог одной традиции: $culture. Другие культуры запрещены. '
|
|
'Пиши по-русски. Если у традиции нет достоверного следа этого образа, '
|
|
'ответь ровно фразой «следа нет» и остановись. Не заполняй пустоту соседним мифом. '
|
|
'Нужны: исконное слово или знак, транслитерация, имя текста или мифа, '
|
|
'что образ делает, чего не позволяет.\n\n'
|
|
'${_dossier(word)}\n'
|
|
'Традиция: $culture.';
|
|
}
|
|
|
|
String languageSlicePrompt(WordItem word, String language) {
|
|
return 'Ты этимолог одного языка: $language. Другие языки запрещены. '
|
|
'Пиши по-русски. Если у русского слова нет корня в этом языке, '
|
|
'скажи «прямого корня нет» и приведи только ближайшее культурное слово этого языка '
|
|
'для того же предмета, не выдавая его за этимон. '
|
|
'Нужны: форма, транслитерация, сдвиг значения, один источник или памятник.\n\n'
|
|
'${_dossier(word)}\n'
|
|
'Язык: $language.';
|
|
}
|
|
|
|
String traditionSlicePrompt(WordItem word, String tradition) {
|
|
return 'Ты хранитель одной системы: $tradition. Другие системы запрещены. '
|
|
'Пиши по-русски. Если этот знак сам не является картой, руной, фигурой или станцией '
|
|
'этой системы, не занимай чужую станцию: напиши, чем знак лишь сосед, '
|
|
'или что прямого места нет.\n\n'
|
|
'${_dossier(word)}\n'
|
|
'Система: $tradition.\n'
|
|
'Ответ: 2–5 предложений закона именно этого слова внутри системы.';
|
|
}
|
|
|
|
String rolePrompt(String roleId, WordItem word) {
|
|
switch (roleId) {
|
|
case 'route_cultures':
|
|
return cultureRoutePrompt(word);
|
|
case 'route_languages':
|
|
return languageRoutePrompt(word);
|
|
case 'mystic':
|
|
return mysticPrompt(word);
|
|
case 'psycho':
|
|
return psychoPrompt(word);
|
|
default:
|
|
return _dossier(word);
|
|
}
|
|
}
|
|
|
|
String synthesizerPrompt({
|
|
required WordItem word,
|
|
required Map<String, String> reports,
|
|
required String core,
|
|
required String oracle,
|
|
required String shadow,
|
|
required String asks,
|
|
required String step,
|
|
required String binds,
|
|
required Map<String, String> cultures,
|
|
required Map<String, String> languages,
|
|
required Map<String, String> traditions,
|
|
}) {
|
|
final payload = <String, dynamic>{
|
|
'word': word.word,
|
|
'reports': reports,
|
|
'draft': {
|
|
'core': core,
|
|
'oracle': oracle,
|
|
'shadow': shadow,
|
|
'asks': asks,
|
|
'step': step,
|
|
'binds': binds,
|
|
'cultures': cultures,
|
|
'languages': languages,
|
|
'traditions': traditions,
|
|
},
|
|
};
|
|
return 'Ты архитектор Оракула 999. Сожми уже собранные ответы в один JSON без markdown. '
|
|
'Не выдумывай культуру, язык или систему, если во входе пусто или стоит «следа нет».\n'
|
|
'Ключи: word, core, oracle, shadow, asks, step, binds, layers, cultures, languages.\n'
|
|
'core — одно предложение, закон знака.\n'
|
|
'oracle — устройство знака, сосед в поле и цена. Культуры поимённо сюда не копируй.\n'
|
|
'shadow — обратная сторона того же закона.\n'
|
|
'asks — один вопрос. step — один физический жест, не «подумай».\n'
|
|
'binds — до 6 русских слов знаков из тысячи, не сам знак.\n'
|
|
'layers — список {system, title, law}. system из: '
|
|
'${kDiveTraditions.map((s) => s.id).join(', ')}.\n'
|
|
'cultures — объект. Ключ строго из списка традиций. Значение: 2–5 фраз.\n'
|
|
'languages — объект. Ключ — имя языка, значение — этимологическая заметка.\n'
|
|
'Запрещены заготовки: «сакральный символ», «созерцать символ», '
|
|
'«готов ли ты принять этот урок», «ключевую сферу кристаллизации».\n'
|
|
'Никакого текста вокруг JSON.\n\n'
|
|
'${_dossier(word)}\n'
|
|
'${jsonEncode(payload)}';
|
|
}
|
|
|
|
/// Имена из ответа маршрута → точные ключи реестра.
|
|
List<String> matchCultureNames(String answer) {
|
|
final found = <String>[];
|
|
for (final raw in answer.split(RegExp(r'[,;\n]+'))) {
|
|
final token = raw.trim();
|
|
if (token.isEmpty) continue;
|
|
final branch = cultureBranchByName(token);
|
|
if (branch != null && !found.contains(branch.name)) found.add(branch.name);
|
|
}
|
|
return found;
|
|
}
|
|
|
|
List<String> matchLanguageNames(String answer) {
|
|
final known = {for (final slot in kDiveLanguages) slot.id: slot.id};
|
|
final found = <String>[];
|
|
for (final raw in answer.split(RegExp(r'[,;\n]+'))) {
|
|
final token = raw.trim().toLowerCase();
|
|
if (token.isEmpty) continue;
|
|
final name = known[token] ?? _languageAliases[token];
|
|
if (name != null && !found.contains(name)) found.add(name);
|
|
}
|
|
return found;
|
|
}
|
|
|
|
class DiveCanonDraft {
|
|
final String? core;
|
|
final String? oracle;
|
|
final String? shadow;
|
|
final String? asks;
|
|
final String? step;
|
|
final List<String>? binds;
|
|
final Map<String, String>? cultures;
|
|
final Map<String, String>? languages;
|
|
final List<SymbolLayer>? layers;
|
|
|
|
const DiveCanonDraft({
|
|
this.core,
|
|
this.oracle,
|
|
this.shadow,
|
|
this.asks,
|
|
this.step,
|
|
this.binds,
|
|
this.cultures,
|
|
this.languages,
|
|
this.layers,
|
|
});
|
|
}
|
|
|
|
DiveCanonDraft? parseDiveCanon(String text) {
|
|
var src = text.trim();
|
|
if (src.isEmpty) return null;
|
|
if (src.startsWith('```')) {
|
|
src = src.replaceFirst(RegExp(r'^```[a-zA-Z]*\n?'), '');
|
|
if (src.endsWith('```')) src = src.substring(0, src.length - 3).trim();
|
|
}
|
|
final start = src.indexOf('{');
|
|
final end = src.lastIndexOf('}');
|
|
if (start < 0 || end <= start) return null;
|
|
final dynamic decoded;
|
|
try {
|
|
decoded = jsonDecode(src.substring(start, end + 1));
|
|
} catch (_) {
|
|
return null;
|
|
}
|
|
if (decoded is! Map) return null;
|
|
final map = decoded.map((key, value) => MapEntry(key.toString(), value));
|
|
|
|
List<String>? binds;
|
|
final rawBinds = map['binds'];
|
|
if (rawBinds is List) {
|
|
binds = rawBinds.map((e) => e.toString().trim()).where((e) => e.isNotEmpty).toList();
|
|
} else if (rawBinds is String && rawBinds.trim().isNotEmpty) {
|
|
binds = rawBinds.split(RegExp(r'[,;]+')).map((e) => e.trim()).where((e) => e.isNotEmpty).toList();
|
|
}
|
|
|
|
Map<String, String>? cultures;
|
|
if (map['cultures'] is Map) {
|
|
cultures = {};
|
|
for (final entry in (map['cultures'] as Map).entries) {
|
|
final branch = cultureBranchByName(entry.key.toString());
|
|
final key = branch?.name ?? entry.key.toString().trim();
|
|
final value = entry.value?.toString().trim() ?? '';
|
|
if (key.isNotEmpty && value.isNotEmpty && value != 'следа нет') cultures[key] = value;
|
|
}
|
|
}
|
|
|
|
Map<String, String>? languages;
|
|
if (map['languages'] is Map) {
|
|
languages = {};
|
|
for (final entry in (map['languages'] as Map).entries) {
|
|
final names = matchLanguageNames(entry.key.toString());
|
|
final key = names.isEmpty ? entry.key.toString().trim().toLowerCase() : names.first;
|
|
final value = entry.value?.toString().trim() ?? '';
|
|
if (key.isNotEmpty && value.isNotEmpty) languages[key] = value;
|
|
}
|
|
}
|
|
|
|
List<SymbolLayer>? layers;
|
|
if (map['layers'] is List) {
|
|
layers = [];
|
|
for (final item in map['layers'] as List) {
|
|
if (item is! Map) continue;
|
|
final law = item['law']?.toString().trim() ?? '';
|
|
if (law.isEmpty) continue;
|
|
final systemRaw = (item['system'] ?? item['title'] ?? '').toString().trim().toLowerCase();
|
|
DiveSlot? slot;
|
|
for (final candidate in kDiveTraditions) {
|
|
if (candidate.id == systemRaw || candidate.title.toLowerCase() == systemRaw) {
|
|
slot = candidate;
|
|
break;
|
|
}
|
|
}
|
|
if (slot == null) continue;
|
|
layers.add(SymbolLayer(system: slot.id, title: slot.title, law: law));
|
|
}
|
|
}
|
|
|
|
String? textOf(String key) {
|
|
final value = map[key];
|
|
if (value == null) return null;
|
|
final text = value.toString().trim();
|
|
return text.isEmpty ? null : text;
|
|
}
|
|
|
|
return DiveCanonDraft(
|
|
core: textOf('core'),
|
|
oracle: textOf('oracle'),
|
|
shadow: textOf('shadow'),
|
|
asks: textOf('asks'),
|
|
step: textOf('step'),
|
|
binds: binds,
|
|
cultures: cultures,
|
|
languages: languages,
|
|
layers: layers,
|
|
);
|
|
}
|