распознавание: большие раздачи размечаются целиком, файлы вне плана видны
- модель адресует файл номером строки нашего списка вместо копии пути: ответ на 180 файлов вместо ~15k токенов стоит ~2.5k, усечение сотней снято, max_files и max_tokens ушли в [recognition], correction-ретрай больше не переприсылает список - негодный элемент ответа отбрасывается поимённой причиной, обрыв генерации и отказ по размеру запроса названы своими причинами, покрытие плана блокирует авто только при непокрытом видеофайле - раскладка показывает все файлы раздачи со строками «не в плане» и полным порядком сортировки; снимок списка файлов лёг рядом с планом (миграция 0012)
This commit is contained in:
@@ -36,14 +36,14 @@ const schemaText = `Схема ответа (строгий JSON, без markdow
|
||||
"provider_hint": "строка для поиска в базе (НЕ id)",
|
||||
"files": [
|
||||
{
|
||||
"src": "путь файла из списка ниже, БЕЗ размера в скобках в конце строки",
|
||||
"i": номер файла из списка ниже (целое, ровно как напечатано),
|
||||
"role": "main" | "episode" | "subtitle" | "extra" | "sample" | "ignore",
|
||||
"season": число или null,
|
||||
"episode": число или null
|
||||
}
|
||||
],
|
||||
"confidence": число 0..1,
|
||||
"notes": "пояснения и неоднозначности или пустая строка"
|
||||
"notes": "неоднозначности, не больше пары предложений, или пустая строка"
|
||||
}
|
||||
|
||||
Правила:
|
||||
@@ -53,10 +53,12 @@ const schemaText = `Схема ответа (строгий JSON, без markdow
|
||||
происхождения — продублируй "title" в "original_title". Если НЕ уверен в
|
||||
оригинальном названии — продублируй "title", но НЕ выдумывай название.
|
||||
- "files" покрывает каждый значимый файл; семплы/мусор помечай ролью "sample"/"ignore".
|
||||
- Поле "i" — номер файла из напечатанного ниже списка, ровно как он там стоит.
|
||||
Путь файла копировать НЕ нужно: он у нас уже есть, мы подставим его сами.
|
||||
- Один и тот же файл адресуй не больше одного раза.
|
||||
- Для сериала каждой серии — отдельный файл с role "episode" и заполненными season и episode.
|
||||
- Для фильма ровно один основной видеофайл role "main".
|
||||
- Поле src — это путь файла из списка, скопированный дословно, но БЕЗ размера
|
||||
«(…)» в конце строки; не выдумывай и не нормализуй пути.
|
||||
- Для ролей "sample", "ignore" и "extra" номера сезона и серии не нужны — оставляй null.
|
||||
- Внешние субтитры — role "subtitle".`
|
||||
|
||||
const systemPromptBase = `Ты распознаёшь медиа-раздачи для медиатеки Jellyfin: по имени торрента,
|
||||
@@ -89,15 +91,18 @@ func systemPrompt(lang string) string {
|
||||
return systemPromptBase + schemaText + languageDirective(lang)
|
||||
}
|
||||
|
||||
// buildMessages собирает системное и пользовательское сообщения.
|
||||
func buildMessages(in Input, pre PreParse, maxFiles int, lang string) []llm.Message {
|
||||
// buildMessages собирает системное и пользовательское сообщения. total —
|
||||
// полное число файлов раздачи (in.Files уже упорядочен и, возможно, усечён
|
||||
// пределом): список печатается ровно тем срезом, по которому потом резолвятся
|
||||
// номера.
|
||||
func buildMessages(in Input, pre PreParse, total int, lang string) []llm.Message {
|
||||
return []llm.Message{
|
||||
{Role: llm.RoleSystem, Content: systemPrompt(lang)},
|
||||
{Role: llm.RoleUser, Content: userPrompt(in, pre, maxFiles)},
|
||||
{Role: llm.RoleUser, Content: userPrompt(in, pre, total)},
|
||||
}
|
||||
}
|
||||
|
||||
func userPrompt(in Input, pre PreParse, maxFiles int) string {
|
||||
func userPrompt(in Input, pre PreParse, total int) string {
|
||||
var b strings.Builder
|
||||
b.WriteString("Имя торрента: ")
|
||||
b.WriteString(orNone(in.Name))
|
||||
@@ -122,7 +127,7 @@ func userPrompt(in Input, pre PreParse, maxFiles int) string {
|
||||
b.WriteString(preParseLine(pre))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
writeFileList(&b, in.Files, maxFiles)
|
||||
writeFileList(&b, in.Files, total)
|
||||
return b.String()
|
||||
}
|
||||
|
||||
@@ -149,19 +154,15 @@ func preParseLine(pre PreParse) string {
|
||||
return strings.Join(parts, ", ")
|
||||
}
|
||||
|
||||
// writeFileList печатает список файлов, усекая до maxFiles. src в плане
|
||||
// должен дословно совпадать с путями отсюда.
|
||||
func writeFileList(b *strings.Builder, files []File, maxFiles int) {
|
||||
n := len(files)
|
||||
shown := n
|
||||
if maxFiles > 0 && shown > maxFiles {
|
||||
shown = maxFiles
|
||||
}
|
||||
b.WriteString("Файлы (")
|
||||
b.WriteString(strconv.Itoa(n))
|
||||
b.WriteString("). В src копируй ТОЛЬКО путь — текст после номера и до размера ")
|
||||
b.WriteString("в скобках; размер «(…)» в конце строки в src НЕ включай:\n")
|
||||
for i := 0; i < shown; i++ {
|
||||
// writeFileList печатает нумерованный список файлов. Номер строки — то, что
|
||||
// модель возвращает в поле "i"; усечение (если оно было) сделал вызывающий,
|
||||
// total — полное число файлов раздачи.
|
||||
func writeFileList(b *strings.Builder, files []File, total int) {
|
||||
b.WriteString("Файлы раздачи (")
|
||||
b.WriteString(strconv.Itoa(len(files)))
|
||||
b.WriteString("). Номер в начале строки — это то, что нужно вернуть в поле \"i\";")
|
||||
b.WriteString(" путь и размер копировать не нужно:\n")
|
||||
for i := range files {
|
||||
b.WriteString(strconv.Itoa(i + 1))
|
||||
b.WriteString(". ")
|
||||
b.WriteString(files[i].Path)
|
||||
@@ -169,22 +170,24 @@ func writeFileList(b *strings.Builder, files []File, maxFiles int) {
|
||||
b.WriteString(humanSize(files[i].Size))
|
||||
b.WriteString(")\n")
|
||||
}
|
||||
if shown < n {
|
||||
if total > len(files) {
|
||||
b.WriteString("… и ещё ")
|
||||
b.WriteString(strconv.Itoa(n - shown))
|
||||
b.WriteString(strconv.Itoa(total - len(files)))
|
||||
b.WriteString(" файлов (список усечён)\n")
|
||||
}
|
||||
}
|
||||
|
||||
// correctionMessage — сообщение для повторной попытки: что было не так + схема.
|
||||
func correctionMessage(err error, in Input, maxFiles int) string {
|
||||
// Список файлов сюда НЕ входит: его номера названы в первом сообщении диалога
|
||||
// и сохраняют смысл на всех попытках, а повторная печать умножала бы вход на
|
||||
// число попыток — ровно ту цену, которую снимает индексная адресация.
|
||||
func correctionMessage(err error) string {
|
||||
var b strings.Builder
|
||||
b.WriteString("Ответ не принят: ")
|
||||
b.WriteString(err.Error())
|
||||
b.WriteString("\nВерни ИСПРАВЛЕННЫЙ ответ строго по схеме, только JSON.\n\n")
|
||||
b.WriteString("\nВерни ИСПРАВЛЕННЫЙ ответ строго по схеме, только JSON.")
|
||||
b.WriteString(" Файлы адресуй номерами из списка, присланного выше.\n\n")
|
||||
b.WriteString(schemaText)
|
||||
b.WriteString("\n\n")
|
||||
writeFileList(&b, in.Files, maxFiles)
|
||||
return b.String()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user