Слияние claude/advisor-opus-78114c: сегментатор страниц, URL/FlareSolverr, агрегат релиза
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
commit
6a87bc993c
13 files changed
+1470
-18
No files matched your search
@@ -0,0 +1,903 @@
|
|||||||
|
<html lang="ru"><head>
|
||||||
|
|
||||||
|
<meta charset="Windows-1251">
|
||||||
|
<meta name="robots" content="nofollow">
|
||||||
|
<link rel="canonical" href="https://rutracker.org/forum/viewtopic.php?t=6870900">
|
||||||
|
|
||||||
|
<title>Супер Марио: Галактическое кино / The Super Mario Galaxy Movie (Аарон Хорват / Aaron Horvath) [2026, Япония, США, мультфильм, фэнтези, WEB-DL 2160p, HDR10, HDR10+, Dolby Vision] [Hybrid] Dub (Продубляж) + 2x MVO + Dub (Ukr) + Original (Eng) + Sub (Rus, Ukr, Eng) :: RuTracker.org</title>
|
||||||
|
|
||||||
|
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon">
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
||||||
|
<link rel="manifest" href="/manifest.json">
|
||||||
|
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
|
||||||
|
<meta name="msapplication-TileColor" content="#2b5797">
|
||||||
|
<meta name="msapplication-TileImage" content="/mstile-144x144.png">
|
||||||
|
<meta name="theme-color" content="#ffffff">
|
||||||
|
|
||||||
|
<link rel="search" type="application/opensearchdescription+xml" title="Поиск на rutracker.org" href="https://static.rutracker.cc/opensearch.xml">
|
||||||
|
<script>
|
||||||
|
window.BB = {
|
||||||
|
mainDomain: 'rutracker.org',
|
||||||
|
cur_domain: location.hostname.replace(/.*?(([a-z0-9-]+\.){1,2}[a-z0-9-]+)$/, '$1'),
|
||||||
|
form_token: '',
|
||||||
|
opt_js: {},
|
||||||
|
|
||||||
|
IS_GUEST: !!'1',
|
||||||
|
IMG_URL: 'https://static.rutracker.cc/templates/v1/images',
|
||||||
|
SMILES_URL: 'https://static.rutracker.cc/smiles',
|
||||||
|
catId: 2,
|
||||||
|
FORUM_ID: 4,
|
||||||
|
parentForumId: 0,
|
||||||
|
COOKIE_MARK: 'bb_mark_read',
|
||||||
|
};
|
||||||
|
BB.cookie_defaults = {
|
||||||
|
domain: '.' + BB.cur_domain,
|
||||||
|
path: "/forum/",
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<link href="https://static.rutracker.cc/templates/v1/min/fdf3950198e99ab3584add652dceac26.all.min.css" rel="stylesheet">
|
||||||
|
<script src="https://static.rutracker.cc/templates/v1/min/4e695e8ea9cf5a1dcc7aed231b887c51.lib.min.js"></script>
|
||||||
|
<script src="https://static.rutracker.cc/templates/v1/min/e0cee5d0f2c39a24f6b4233f4c6e85f8.bb.min.js"></script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.invisible-el {
|
||||||
|
/* @include element-invisible(); */
|
||||||
|
clip: rect(1px, 1px, 1px, 1px);
|
||||||
|
height: 1px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: absolute !important;
|
||||||
|
width: 1px;
|
||||||
|
}
|
||||||
|
.hidden, .menu-sub, #ajax-loading, #ajax-error, var.ajax-params, #modal-blocker, .q-post {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
/**/
|
||||||
|
#logo {
|
||||||
|
min-height: auto;
|
||||||
|
}
|
||||||
|
/**/
|
||||||
|
/* temp */
|
||||||
|
/* temp end */
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<noscript>
|
||||||
|
<style>
|
||||||
|
.sp-body {
|
||||||
|
display: block; /* for bots */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</noscript>
|
||||||
|
|
||||||
|
<meta class="foundation-mq"></head>
|
||||||
|
|
||||||
|
<body class="" style="">
|
||||||
|
|
||||||
|
<div id="invisible-heap" class="invisible-el"></div>
|
||||||
|
<div id="preload" class="invisible-el"></div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div id="body_container">
|
||||||
|
|
||||||
|
<script>
|
||||||
|
if (top != self && !self.location.hostname.match(BB.allowed_translator_hosts)) {
|
||||||
|
$(function () {
|
||||||
|
$('body').html('<h1 style="text-align: center;"><br><br>Похоже, вас пытаются обмануть<br>frame\'s hostname: ' + self.location.hostname + '</h1>');
|
||||||
|
throw new Error('in frame');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div id="page_container">
|
||||||
|
|
||||||
|
<div id="page_header" class="hide-for-print">
|
||||||
|
|
||||||
|
<div id="main-nav" class="site-nav clearfix">
|
||||||
|
<ul class="inlined middot-separated floatL">
|
||||||
|
<li>
|
||||||
|
<a href="index.php"><b>Главная</b></a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="tracker.php" rel="nofollow"><b>Трекер</b></a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="search.php" rel="nofollow"><b>Поиск</b></a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="groupcp.php" rel="nofollow"><b>Группы</b></a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="https://rutracker.wiki/" target="_blank"><b>FAQ</b></a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="viewtopic.php?t=6812335" style="color: #7b1fa2;"><b>VPN</b></a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="viewtopic.php?t=1045"><b>Правила</b></a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="viewtopic.php?t=6452965" style="color: #880e4f;"><b>Донаты</b></a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<!--/main-nav-->
|
||||||
|
|
||||||
|
|
||||||
|
<div id="logo">
|
||||||
|
<table style="">
|
||||||
|
<tbody><tr>
|
||||||
|
<td id="logo-td" style="padding: 3px 7px 3px 15px;">
|
||||||
|
<a href="index.php" class="site-logo-link">
|
||||||
|
<img class="site-logo" src="https://static.rutracker.cc/logo/logo-3.svg" style="" alt="logo">
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td class="tCenter w100" style="padding: 6px 10px 0 3px;">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody></table>
|
||||||
|
</div>
|
||||||
|
<!--/logo-->
|
||||||
|
|
||||||
|
<div class="topmenu">
|
||||||
|
<table class="w100">
|
||||||
|
<tbody><tr>
|
||||||
|
|
||||||
|
<td class="tCenter pad_2 nowrap">
|
||||||
|
<div>
|
||||||
|
<a href="profile.php?mode=register" rel="nofollow"><b>Регистрация</b></a>
|
||||||
|
·
|
||||||
|
<a href="#" onclick="BB.toggle_top_login(); return false;" tabindex="5"><b>Вход</b></a>
|
||||||
|
|
||||||
|
<div id="top-login-box">
|
||||||
|
<form id="quick-search-guest" method="get" action="tracker.php">
|
||||||
|
<input id="search-text-guest" type="text" name="nm" value="" tabindex="1" accesskey="q">
|
||||||
|
<input type="submit" value="поиск" tabindex="2">
|
||||||
|
</form>
|
||||||
|
<form id="login-form-quick" method="post" action="login.php" style="display: none;">
|
||||||
|
<input id="top-login-uname" type="text" name="login_username" tabindex="1" accesskey="l" placeholder="имя">
|
||||||
|
<input id="top-login-pwd" type="password" name="login_password" tabindex="2" placeholder="пароль">
|
||||||
|
<label title="Защищенное соединение" class="login-ssl-block" style="display: none;">
|
||||||
|
<input class="login-ssl" type="checkbox" tabindex="3" checked="">SSL
|
||||||
|
</label>
|
||||||
|
<input id="top-login-btn" type="submit" name="login" value="вход" tabindex="4">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="otp.php" rel="nofollow"><b>Забыли имя или пароль?</b></a>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
|
||||||
|
</tr>
|
||||||
|
</tbody></table>
|
||||||
|
</div><!--/topmenu-->
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<!--/page_header-->
|
||||||
|
|
||||||
|
|
||||||
|
<div id="page_content">
|
||||||
|
<table style="width: 100%; border: none;">
|
||||||
|
<tbody><tr>
|
||||||
|
|
||||||
|
|
||||||
|
<td id="main_content">
|
||||||
|
<div id="main_content_wrap">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!--/page_header.tpl-->
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
$(function () {
|
||||||
|
BB.initMagnetLinks();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<table class="w100">
|
||||||
|
<tbody><tr>
|
||||||
|
<td class="w100 vBottom pad_2">
|
||||||
|
<h1 class="maintitle">
|
||||||
|
<a id="topic-title" class="topic-title-6870900 highlight-cyrillic" href="https://rutracker.org/forum/viewtopic.php?t=6870900"><span class="cyrillic-char">Супер</span> <span class="cyrillic-char">Марио</span>: <span class="cyrillic-char">Галактическо</span><wbr><span class="cyrillic-char">е</span> <span class="cyrillic-char">кино</span> / The Super Mario Galaxy Movie (<span class="cyrillic-char">Аарон</span> <span class="cyrillic-char">Хорват</span> / Aaron Horvath) [2026, <span class="cyrillic-char">Япония</span>, <span class="cyrillic-char">США</span>, <span class="cyrillic-char">мультфильм</span>, <span class="cyrillic-char">фэнтези</span>, WEB-DL 2160p, HDR10, HDR10+, Dolby Vision] [Hybrid] Dub (<span class="cyrillic-char">Продубляж</span>) + 2x MVO + Dub (Ukr) + Original (Eng) + Sub (Rus, Ukr, Eng)</a>
|
||||||
|
</h1>
|
||||||
|
<div class="small hide-for-print" style="margin: 12px 4px 8px;">
|
||||||
|
<b> Страницы: 1</b>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table class="w100">
|
||||||
|
<tbody><tr>
|
||||||
|
<td class="pad_2 hide-for-print">
|
||||||
|
<a href="posting.php?mode=reply&t=6870900"><img src="https://static.rutracker.cc/templates/v1/images/reply.gif" alt="Ответить"></a>
|
||||||
|
</td>
|
||||||
|
<td class="nav t-breadcrumb-top w100 pad_2">
|
||||||
|
<a href="index.php?c=2">Кино, Видео и ТВ</a>
|
||||||
|
<em>»</em> <a href="viewforum.php?f=4">Мультфильмы</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody></table>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td class="pad_2 hide-for-print">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody></table>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<table class="topic" id="topic_main">
|
||||||
|
<tbody class="hide-for-print">
|
||||||
|
<tr>
|
||||||
|
<th colspan="2" class="thHead">
|
||||||
|
<div id="soc-container" data-share_url="https://rutracker.org/forum/viewtopic.php?t=6870900" data-share_title="Супер Марио: Галактическое кино / The Super Mario Galaxy Movie (Аарон Хорват / Aaron Horvath) [2026, Япония, США, мультфильм, фэнтези, WEB-DL 2160p, HDR10, HDR10+, Dolby Vision] [Hybrid] Dub (Продубляж) + 2x MVO + Dub (Ukr) + Original (Eng) + Sub (Rus, Ukr, Eng)"> <div class="soc-buttons"><span title="Опубликовать ссылку в Facebook" style="background-position: 0px center;"></span><span title="Опубликовать ссылку в Twitter" style="background-position: -18px center;"></span><span title="Опубликовать ссылку в Вконтакте" style="background-position: -36px center;"></span><span title="Опубликовать ссылку в LiveJournal" style="background-position: -54px center;"></span></div></div>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<tbody id="post_89278840" class="row1">
|
||||||
|
<tr>
|
||||||
|
<td class="poster_info td1 hide-for-print">
|
||||||
|
<a id="89278840"></a>
|
||||||
|
|
||||||
|
|
||||||
|
<p class="nick nick-author">Andron1975</p>
|
||||||
|
|
||||||
|
|
||||||
|
<p class="avatar"><img src="https://static.rutracker.cc/avatars/2/28/13870228.jpg" alt=""></p> <p class="joined"><em>Стаж:</em> 16 лет 10 месяцев</p> <p class="posts"><em>Сообщений:</em> 868</p>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td class="message td2" rowspan="2">
|
||||||
|
|
||||||
|
<div class="post_head">
|
||||||
|
<p class="post-time">
|
||||||
|
<span class="hl-scrolled-to-wrap">
|
||||||
|
<span class="show-for-print bold">Andron1975 · </span>
|
||||||
|
<img src="https://static.rutracker.cc/templates/v1/images/icon_minipost.gif" class="icon1 hide-for-print" alt="">
|
||||||
|
<a class="p-link small" href="viewtopic.php?t=6870900">15-Июн-26 16:41</a>
|
||||||
|
</span>
|
||||||
|
<span class="posted_since hide-for-print">(4 месяца 11 дней назад)</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul class="inlined t-post-buttons hide-for-print" style="float: right; padding: 3px 2px 4px;">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
<div class="clear"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="post_wrap" id="p-20862-1">
|
||||||
|
<div class="post_body" id="p-89278840" data-ext_link_data="{"p":89278840,"t":6870900,"f":4,"u":13870228}">
|
||||||
|
<span class="post-font-serif1"><span class="post-align" style="text-align: center;"><img class="postImg" alt="pic" src="https://i.postimg.cc/W4wYJwHJ/the-super-mario-galaxy-movie-banner.jpg"></span><span style="font-size: 24px; line-height: normal;"><span class="post-align" style="text-align: center;"><span class="post-b"><span class="post-i">Супер Марио: Галактическое кино / <span class="p-color" style="color: blue;">The Super Mario Galaxy Movie</span></span></span></span></span><span class="post-align" style="text-align: center;"><span style="font-size: 12px; line-height: normal;"><span class="p-color" style="color: gray;">«New galaxies. New friends. Yoshi joins the adventure.»</span></span></span><hr class="post-hr"><hr class="post-hr"><img class="postImg postImgAligned img-right" alt="pic" src="https://i.postimg.cc/qRmm1M0z/the-super-mario-galaxy-movie-rutracker.jpg"><span class="post-i"><span class="post-b">Страна</span></span>: Япония, США<br>
|
||||||
|
<span class="post-i"><span class="post-b">Студия</span></span>: Universal Pictures, Nintendo, Illumination Entertainment<br>
|
||||||
|
<span class="post-i"><span class="post-b">Жанр</span></span>: мультфильм, фэнтези, комедия, приключения, семейный<br>
|
||||||
|
<span class="post-i"><span class="post-b">Год выпуска</span></span>: 2026<br>
|
||||||
|
<span class="post-i"><span class="post-b">Продолжительность</span></span>: 01:38:12<hr class="post-hr"><span class="post-i"><span class="post-b">Перевод 1</span></span>: Профессиональный (дублированный) <span class="p-color" style="color: blue;"><span class="post-b">Продубляж</span></span><br>
|
||||||
|
<span class="post-i"><span class="post-b">Перевод 2</span></span>: Профессиональный (многоголосый закадровый) <span class="p-color" style="color: blue;"><span class="post-b">AlexFilm</span></span><br>
|
||||||
|
<span class="post-i"><span class="post-b">Перевод 3</span></span>: Любительский (многоголосый закадровый) <span class="p-color" style="color: blue;"><span class="post-b">Дубляжная</span></span><br>
|
||||||
|
<span class="post-i"><span class="post-b">Перевод 4</span></span>: Профессиональный (дублированный) <span class="p-color" style="color: blue;"><span class="post-b">Le Doyen</span></span><br>
|
||||||
|
<span class="post-i"><span class="post-b">Субтитры</span></span>: Русские (Forced), Украинские (Forced, 2x Full, SDH), Английские (Full, SDH)<br>
|
||||||
|
<span class="post-i"><span class="post-b">Оригинальная аудиодорожка</span></span>: Aнглийский<hr class="post-hr"><span class="post-i"><span class="post-b">Режиссер</span></span>: Аарон Хорват, Михаэль Еленик, Пьер Ледюк, Фабьен Полак / Aaron Horvath, Michael Jelenic, Pierre Leduc, Fabien Polack<br>
|
||||||
|
<span class="post-i"><span class="post-b">В ролях</span></span>: Крис Пратт, Дональд Гловер, Бри Ларсон, Вирджиния Даре Еленик, Бенни Сэфди, Кевин Майкл Ричардсон, Чарли Дэй, Кигэн-Майкл Ки, Аня Тейлор-Джой, Эрик Бауза, Джек Блэк, Роксана Ортега, Луис Гусман, Исса Рэй, Глен Пауэлл<hr class="post-hr"><span class="post-i"><span class="post-b">Описание</span></span>: Боузер-младший похищает принцессу Розалину и пытается вернуть отца, создав пушку, способную уничтожать вселенные. Марио, Луиджи и найденный ими дракон Йоши пытаются перевоспитать поверженного Боузера, но вскоре того освобождает сын. Водопроводчик объединяет силы со своими друзьями, чтобы помешать планам Боузера-младшего по достижению вселенского господства.<hr class="post-hr"><span class="p-color" style="color: darkred;"><span class="post-b"><span style="font-size: 16px; line-height: normal;">*</span></span></span></span>
|
||||||
|
<div class="post-box-default">
|
||||||
|
<div class="post-box" style="border-color: #FF0000; background: #FFCCCC;">Дорожка №2 получена путём наложения чистых голосов на центр декодированного оригинала. Работа со звуком - <span class="post-b">Andron1975</span> (DVT).<br>
|
||||||
|
Спасибо студии <span class="post-b">AlexFilm</span> за предоставленные голоса.<br>
|
||||||
|
За дубляж, Дубляжную и forced субтитры спасибо <span class="post-b">Wilmots</span>.<br>
|
||||||
|
За украинский дубляж и субтитры спасибо <span class="post-b">Utopia</span>.</div>
|
||||||
|
</div>
|
||||||
|
<hr class="post-hr"><span class="post-align" style="text-align: center;"><a href="https://www.imdb.com/title/tt28650488/" class="postLink p-ext-link"><img class="postImg" alt="pic" src="https://static.rutracker.cc/pic/buttons/imdb.png"></a> <a href="https://www.kinopoisk.ru/film/5934685/" class="postLink p-ext-link"><img class="postImg" alt="pic" src="https://rating.kinopoisk.ru/5934685.gif"></a> <a href="https://drive.proton.me/urls/GYXECWWTJ8#RmL9Q3DJbARt" class="postLink p-ext-link"><img class="postImg" alt="pic" src="http://i67.fastpic.org/big/2014/0715/fc/31eebf12bf307aede4b096788b44aefc.png"> <img class="postImg" alt="pic" src="https://i.postimg.cc/6QCJhD8t/Alex-Film-logo.jpg"></a></span> <hr class="post-hr"><span class="post-i"><span class="post-b">Тип релиза</span></span>: WEB-DL 2160p <span class="post-b">Dolby Vision</span><br>
|
||||||
|
<span class="post-i"><span class="post-b">Контейнер</span></span>: MKV<br>
|
||||||
|
<span class="post-i"><span class="post-b">Видео</span></span>: MPEG-H HEVC Video / 24,8 Mbps / 3840x2160 / 23.976 fps / 16:9 / Main 10@L5@High / 10 bits <span class="post-b">/ Dolby Vision - Profile 8 / HDR10+ / HDR10 /</span><br>
|
||||||
|
<span class="post-i"><span class="post-b">Аудио 1</span></span>: <span class="p-color" style="color: blue;"><span class="post-b">Russian</span></span> E-AC3 / 5.1 / 48 kHz / 768 kbps <span class="p-color" style="color: blue;"><span class="post-b">|Дубляж, Продубляж|</span></span><br>
|
||||||
|
<span class="post-i"><span class="post-b">Аудио 2</span></span>: <span class="p-color" style="color: blue;"><span class="post-b">Russian</span></span> E-AC3+Atmos / 5.1 / 48 kHz / 768 kbps <span class="p-color" style="color: blue;"><span class="post-b">|MVO, AlexFilm|</span></span><br>
|
||||||
|
<span class="post-i"><span class="post-b">Аудио 3</span></span>: <span class="p-color" style="color: blue;"><span class="post-b">Russian</span></span> AC3 / 5.1 / 48 kHz / 640 kbps <span class="p-color" style="color: blue;"><span class="post-b">|MVO, Дубляжная|</span></span><br>
|
||||||
|
<span class="post-i"><span class="post-b">Аудио 4</span></span>: <span class="p-color" style="color: blue;"><span class="post-b">Ukrainian</span></span> E-AC3 / 5.1 / 48 kHz / 256 kbps <span class="p-color" style="color: blue;"><span class="post-b">|Дубляж, Le Doyen|</span></span><br>
|
||||||
|
<span class="post-i"><span class="post-b">Аудио 5</span></span>: <span class="p-color" style="color: blue;"><span class="post-b">English</span></span> E-AC3+Atmos / 5.1 / 48 kHz / 768 kbps <span class="p-color" style="color: blue;"><span class="post-b">|Original|</span></span><br>
|
||||||
|
<span class="post-i"><span class="post-b">Формат субтитров</span></span>: softsub (SRT)<br>
|
||||||
|
<span class="post-b">Реклама:</span>
|
||||||
|
<div class="post-box-default">
|
||||||
|
<div class="post-box"><span class="p-color" style="color: black;"><span class="post-b">Отсутствует</span></span></div>
|
||||||
|
</div>
|
||||||
|
<hr class="post-hr"><span class="post-i"><span class="post-b">Релиз от</span></span>: <img class="postImg" alt="pic" src="https://i.postimg.cc/PxtbW31p/DVT-group-logo.gif"><hr class="post-hr">
|
||||||
|
<div class="sp-wrap">
|
||||||
|
<div class="sp-head folded"><span>MediaInfo</span></div>
|
||||||
|
<div class="sp-body">
|
||||||
|
<div class="c-wrap">
|
||||||
|
<div class="c-head"><b>Код:</b></div>
|
||||||
|
<div class="c-body">General<br>
|
||||||
|
Unique ID : 186297677512139737153141175159574550962 (0x8C27A0A85D62281951A7219FE5F0D9B2)<br>
|
||||||
|
Complete name : Z:\TORRENTS\The.Super.Mario.Galaxy.Movie.2026.2160p.MA.WEB-DL.DDP5.1.Atmos.DV.HDRP-DVT.mkv<br>
|
||||||
|
Format : Matroska<br>
|
||||||
|
Format version : Version 4<br>
|
||||||
|
File size : 19.2 GiB<br>
|
||||||
|
Duration : 1 h 38 min<br>
|
||||||
|
Overall bit rate : 28.0 Mb/s<br>
|
||||||
|
Frame rate : 23.976 FPS<br>
|
||||||
|
Movie name : The Super Mario Galaxy Movie (2026) - Release by DVT<br>
|
||||||
|
Encoded date : 2026-06-14 14:59:12 UTC<br>
|
||||||
|
Writing application : mkvmerge v99.0 ('Buka') 64-bit<br>
|
||||||
|
Writing library : libebml v1.4.5 + libmatroska v1.7.1<br>
|
||||||
|
Cover : Yes<br>
|
||||||
|
Attachments : cover.jpg<span class="post-br"><br></span>Video<br>
|
||||||
|
ID : 1<br>
|
||||||
|
Format : HEVC<br>
|
||||||
|
Format/Info : High Efficiency Video Coding<br>
|
||||||
|
Format profile : Main 10@L5@High<br>
|
||||||
|
HDR format : Dolby Vision, Version 1.0, Profile 8.1, dvhe.08.06, BL+RPU, no metadata compression, HDR10 compatible / SMPTE ST 2094 App 4, Version HDR10+ Profile B, HDR10+ Profile B compatible<br>
|
||||||
|
Codec ID : V_MPEGH/ISO/HEVC<br>
|
||||||
|
Duration : 1 h 38 min<br>
|
||||||
|
Bit rate : 24.8 Mb/s<br>
|
||||||
|
Width : 3 840 pixels<br>
|
||||||
|
Height : 2 160 pixels<br>
|
||||||
|
Display aspect ratio : 16:9<br>
|
||||||
|
Frame rate mode : Constant<br>
|
||||||
|
Frame rate : 23.976 (24000/1001) FPS<br>
|
||||||
|
Color space : YUV<br>
|
||||||
|
Chroma subsampling : 4:2:0 (Type 2)<br>
|
||||||
|
Bit depth : 10 bits<br>
|
||||||
|
Bits/(Pixel*Frame) : 0.125<br>
|
||||||
|
Stream size : 17.0 GiB (89%)<br>
|
||||||
|
Title : The Super Mario Galaxy Movie (2026)<br>
|
||||||
|
Language : English<br>
|
||||||
|
Default : Yes<br>
|
||||||
|
Forced : No<br>
|
||||||
|
Color range : Limited<br>
|
||||||
|
Color primaries : BT.2020<br>
|
||||||
|
Transfer characteristics : PQ<br>
|
||||||
|
Matrix coefficients : BT.2020 non-constant<br>
|
||||||
|
Mastering display color primaries : BT.2020<br>
|
||||||
|
Mastering display luminance : min: 0.0010 cd/m2, max: 1000 cd/m2<br>
|
||||||
|
Maximum Content Light Level : 1000 cd/m2<br>
|
||||||
|
Maximum Frame-Average Light Level : 475 cd/m2<span class="post-br"><br></span>Audio #1<br>
|
||||||
|
ID : 2<br>
|
||||||
|
Format : E-AC-3<br>
|
||||||
|
Format/Info : Enhanced AC-3<br>
|
||||||
|
Commercial name : Dolby Digital Plus<br>
|
||||||
|
Codec ID : A_EAC3<br>
|
||||||
|
Duration : 1 h 38 min<br>
|
||||||
|
Bit rate mode : Constant<br>
|
||||||
|
Bit rate : 768 kb/s<br>
|
||||||
|
Channel(s) : 6 channels<br>
|
||||||
|
Channel layout : L R C LFE Ls Rs<br>
|
||||||
|
Sampling rate : 48.0 kHz<br>
|
||||||
|
Frame rate : 31.250 FPS (1536 SPF)<br>
|
||||||
|
Compression mode : Lossy<br>
|
||||||
|
Stream size : 539 MiB (3%)<br>
|
||||||
|
Title : Дубляж, Продубляж<br>
|
||||||
|
Language : Russian<br>
|
||||||
|
Service kind : Complete Main<br>
|
||||||
|
Default : Yes<br>
|
||||||
|
Forced : No<br>
|
||||||
|
Dialog Normalization : -29 dB<br>
|
||||||
|
compr : -0.28 dB<br>
|
||||||
|
dialnorm_Average : -29 dB<br>
|
||||||
|
dialnorm_Minimum : -29 dB<br>
|
||||||
|
dialnorm_Maximum : -29 dB<span class="post-br"><br></span>Audio #2<br>
|
||||||
|
ID : 3<br>
|
||||||
|
Format : E-AC-3 JOC<br>
|
||||||
|
Format/Info : Enhanced AC-3 with Joint Object Coding<br>
|
||||||
|
Commercial name : Dolby Digital Plus with Dolby Atmos<br>
|
||||||
|
Codec ID : A_EAC3<br>
|
||||||
|
Duration : 1 h 38 min<br>
|
||||||
|
Bit rate mode : Constant<br>
|
||||||
|
Bit rate : 768 kb/s<br>
|
||||||
|
Channel(s) : 6 channels<br>
|
||||||
|
Channel layout : L R C LFE Ls Rs<br>
|
||||||
|
Sampling rate : 48.0 kHz<br>
|
||||||
|
Frame rate : 31.250 FPS (1536 SPF)<br>
|
||||||
|
Compression mode : Lossy<br>
|
||||||
|
Stream size : 539 MiB (3%)<br>
|
||||||
|
Title : MVO, AlexFilm<br>
|
||||||
|
Language : Russian<br>
|
||||||
|
Service kind : Complete Main<br>
|
||||||
|
Default : No<br>
|
||||||
|
Forced : No<br>
|
||||||
|
Complexity index : 16<br>
|
||||||
|
Number of dynamic objects : 15<br>
|
||||||
|
Bed channel count : 1 channel<br>
|
||||||
|
Bed channel configuration : LFE<br>
|
||||||
|
Dialog Normalization : -24 dB<br>
|
||||||
|
compr : -0.28 dB<br>
|
||||||
|
dmixmod : Lo/Ro<br>
|
||||||
|
ltrtcmixlev : -3.0 dB<br>
|
||||||
|
ltrtsurmixlev : -3.0 dB<br>
|
||||||
|
lorocmixlev : -3.0 dB<br>
|
||||||
|
lorosurmixlev : -3.0 dB<br>
|
||||||
|
dialnorm_Average : -24 dB<br>
|
||||||
|
dialnorm_Minimum : -24 dB<br>
|
||||||
|
dialnorm_Maximum : -24 dB<span class="post-br"><br></span>Audio #3<br>
|
||||||
|
ID : 4<br>
|
||||||
|
Format : AC-3<br>
|
||||||
|
Format/Info : Audio Coding 3<br>
|
||||||
|
Commercial name : Dolby Digital<br>
|
||||||
|
Codec ID : A_AC3<br>
|
||||||
|
Duration : 1 h 38 min<br>
|
||||||
|
Bit rate mode : Constant<br>
|
||||||
|
Bit rate : 640 kb/s<br>
|
||||||
|
Channel(s) : 6 channels<br>
|
||||||
|
Channel layout : L R C LFE Ls Rs<br>
|
||||||
|
Sampling rate : 48.0 kHz<br>
|
||||||
|
Frame rate : 31.250 FPS (1536 SPF)<br>
|
||||||
|
Compression mode : Lossy<br>
|
||||||
|
Stream size : 450 MiB (2%)<br>
|
||||||
|
Title : MVO, Дубляжная<br>
|
||||||
|
Language : Russian<br>
|
||||||
|
Service kind : Complete Main<br>
|
||||||
|
Default : No<br>
|
||||||
|
Forced : No<br>
|
||||||
|
Dialog Normalization : -31 dB<br>
|
||||||
|
cmixlev : -4.5 dB<br>
|
||||||
|
surmixlev : -6 dB<br>
|
||||||
|
dialnorm_Average : -31 dB<br>
|
||||||
|
dialnorm_Minimum : -31 dB<br>
|
||||||
|
dialnorm_Maximum : -31 dB<span class="post-br"><br></span>Audio #4<br>
|
||||||
|
ID : 5<br>
|
||||||
|
Format : E-AC-3<br>
|
||||||
|
Format/Info : Enhanced AC-3<br>
|
||||||
|
Commercial name : Dolby Digital Plus<br>
|
||||||
|
Codec ID : A_EAC3<br>
|
||||||
|
Duration : 1 h 38 min<br>
|
||||||
|
Bit rate mode : Constant<br>
|
||||||
|
Bit rate : 256 kb/s<br>
|
||||||
|
Channel(s) : 6 channels<br>
|
||||||
|
Channel layout : L R C LFE Ls Rs<br>
|
||||||
|
Sampling rate : 48.0 kHz<br>
|
||||||
|
Frame rate : 31.250 FPS (1536 SPF)<br>
|
||||||
|
Compression mode : Lossy<br>
|
||||||
|
Stream size : 180 MiB (1%)<br>
|
||||||
|
Title : Дубляж, Le Doyen<br>
|
||||||
|
Language : Ukrainian<br>
|
||||||
|
Service kind : Complete Main<br>
|
||||||
|
Default : No<br>
|
||||||
|
Forced : No<br>
|
||||||
|
Dialog Normalization : -23 dB<br>
|
||||||
|
compr : -0.28 dB<br>
|
||||||
|
mixlevel : 105 dB<br>
|
||||||
|
dialnorm_Average : -23 dB<br>
|
||||||
|
dialnorm_Minimum : -23 dB<br>
|
||||||
|
dialnorm_Maximum : -23 dB<span class="post-br"><br></span>Audio #5<br>
|
||||||
|
ID : 6<br>
|
||||||
|
Format : E-AC-3 JOC<br>
|
||||||
|
Format/Info : Enhanced AC-3 with Joint Object Coding<br>
|
||||||
|
Commercial name : Dolby Digital Plus with Dolby Atmos<br>
|
||||||
|
Codec ID : A_EAC3<br>
|
||||||
|
Duration : 1 h 38 min<br>
|
||||||
|
Bit rate mode : Constant<br>
|
||||||
|
Bit rate : 768 kb/s<br>
|
||||||
|
Channel(s) : 6 channels<br>
|
||||||
|
Channel layout : L R C LFE Ls Rs<br>
|
||||||
|
Sampling rate : 48.0 kHz<br>
|
||||||
|
Frame rate : 31.250 FPS (1536 SPF)<br>
|
||||||
|
Compression mode : Lossy<br>
|
||||||
|
Stream size : 539 MiB (3%)<br>
|
||||||
|
Title : Original<br>
|
||||||
|
Language : English<br>
|
||||||
|
Service kind : Complete Main<br>
|
||||||
|
Default : No<br>
|
||||||
|
Forced : No<br>
|
||||||
|
Complexity index : 16<br>
|
||||||
|
Number of dynamic objects : 15<br>
|
||||||
|
Bed channel count : 1 channel<br>
|
||||||
|
Bed channel configuration : LFE<br>
|
||||||
|
Dialog Normalization : -27 dB<br>
|
||||||
|
compr : -0.28 dB<br>
|
||||||
|
dialnorm_Average : -27 dB<br>
|
||||||
|
dialnorm_Minimum : -27 dB<br>
|
||||||
|
dialnorm_Maximum : -27 dB<span class="post-br"><br></span>Text #1<br>
|
||||||
|
ID : 7<br>
|
||||||
|
Format : UTF-8<br>
|
||||||
|
Codec ID : S_TEXT/UTF8<br>
|
||||||
|
Codec ID/Info : UTF-8 Plain Text<br>
|
||||||
|
Duration : 1 h 16 min<br>
|
||||||
|
Bit rate : 0 b/s<br>
|
||||||
|
Frame rate : 0.002 FPS<br>
|
||||||
|
Count of elements : 9<br>
|
||||||
|
Stream size : 316 Bytes (0%)<br>
|
||||||
|
Title : Forced<br>
|
||||||
|
Language : Russian<br>
|
||||||
|
Default : Yes<br>
|
||||||
|
Forced : No<span class="post-br"><br></span>Text #2<br>
|
||||||
|
ID : 8<br>
|
||||||
|
Format : UTF-8<br>
|
||||||
|
Codec ID : S_TEXT/UTF8<br>
|
||||||
|
Codec ID/Info : UTF-8 Plain Text<br>
|
||||||
|
Duration : 1 h 0 min<br>
|
||||||
|
Bit rate : 0 b/s<br>
|
||||||
|
Frame rate : 0.003 FPS<br>
|
||||||
|
Count of elements : 12<br>
|
||||||
|
Stream size : 328 Bytes (0%)<br>
|
||||||
|
Title : Forced<br>
|
||||||
|
Language : Ukrainian<br>
|
||||||
|
Default : No<br>
|
||||||
|
Forced : No<span class="post-br"><br></span>Text #3<br>
|
||||||
|
ID : 9<br>
|
||||||
|
Format : UTF-8<br>
|
||||||
|
Codec ID : S_TEXT/UTF8<br>
|
||||||
|
Codec ID/Info : UTF-8 Plain Text<br>
|
||||||
|
Duration : 1 h 36 min<br>
|
||||||
|
Bit rate : 42 b/s<br>
|
||||||
|
Frame rate : 0.118 FPS<br>
|
||||||
|
Count of elements : 680<br>
|
||||||
|
Stream size : 30.2 KiB (0%)<br>
|
||||||
|
Title : Full<br>
|
||||||
|
Language : Ukrainian<br>
|
||||||
|
Default : No<br>
|
||||||
|
Forced : No<span class="post-br"><br></span>Text #4<br>
|
||||||
|
ID : 10<br>
|
||||||
|
Format : UTF-8<br>
|
||||||
|
Codec ID : S_TEXT/UTF8<br>
|
||||||
|
Codec ID/Info : UTF-8 Plain Text<br>
|
||||||
|
Duration : 1 h 37 min<br>
|
||||||
|
Bit rate : 43 b/s<br>
|
||||||
|
Frame rate : 0.141 FPS<br>
|
||||||
|
Count of elements : 829<br>
|
||||||
|
Stream size : 31.1 KiB (0%)<br>
|
||||||
|
Title : Full #2<br>
|
||||||
|
Language : Ukrainian<br>
|
||||||
|
Default : No<br>
|
||||||
|
Forced : No<span class="post-br"><br></span>Text #5<br>
|
||||||
|
ID : 11<br>
|
||||||
|
Format : UTF-8<br>
|
||||||
|
Codec ID : S_TEXT/UTF8<br>
|
||||||
|
Codec ID/Info : UTF-8 Plain Text<br>
|
||||||
|
Duration : 1 h 38 min<br>
|
||||||
|
Bit rate : 73 b/s<br>
|
||||||
|
Frame rate : 0.289 FPS<br>
|
||||||
|
Count of elements : 1701<br>
|
||||||
|
Stream size : 53.1 KiB (0%)<br>
|
||||||
|
Title : SDH<br>
|
||||||
|
Language : Ukrainian<br>
|
||||||
|
Default : No<br>
|
||||||
|
Forced : No<span class="post-br"><br></span>Text #6<br>
|
||||||
|
ID : 12<br>
|
||||||
|
Format : UTF-8<br>
|
||||||
|
Codec ID : S_TEXT/UTF8<br>
|
||||||
|
Codec ID/Info : UTF-8 Plain Text<br>
|
||||||
|
Duration : 1 h 37 min<br>
|
||||||
|
Bit rate : 28 b/s<br>
|
||||||
|
Frame rate : 0.150 FPS<br>
|
||||||
|
Count of elements : 878<br>
|
||||||
|
Stream size : 20.3 KiB (0%)<br>
|
||||||
|
Title : Full<br>
|
||||||
|
Language : English<br>
|
||||||
|
Default : No<br>
|
||||||
|
Forced : No<span class="post-br"><br></span>Text #7<br>
|
||||||
|
ID : 13<br>
|
||||||
|
Format : UTF-8<br>
|
||||||
|
Codec ID : S_TEXT/UTF8<br>
|
||||||
|
Codec ID/Info : UTF-8 Plain Text<br>
|
||||||
|
Duration : 1 h 38 min<br>
|
||||||
|
Bit rate : 46 b/s<br>
|
||||||
|
Frame rate : 0.280 FPS<br>
|
||||||
|
Count of elements : 1649<br>
|
||||||
|
Stream size : 33.4 KiB (0%)<br>
|
||||||
|
Title : SDH<br>
|
||||||
|
Language : English<br>
|
||||||
|
Default : No<br>
|
||||||
|
Forced : No<span class="post-br"><br></span>Menu<br>
|
||||||
|
00:00:00.000 : en:Chapter 01<br>
|
||||||
|
00:06:29.388 : en:Chapter 02<br>
|
||||||
|
00:12:10.062 : en:Chapter 03<br>
|
||||||
|
00:17:08.277 : en:Chapter 04<br>
|
||||||
|
00:23:43.213 : en:Chapter 05<br>
|
||||||
|
00:29:18.757 : en:Chapter 06<br>
|
||||||
|
00:37:11.313 : en:Chapter 07<br>
|
||||||
|
00:42:40.392 : en:Chapter 08<br>
|
||||||
|
00:47:29.056 : en:Chapter 09<br>
|
||||||
|
00:53:08.020 : en:Chapter 10<br>
|
||||||
|
00:58:31.176 : en:Chapter 11<br>
|
||||||
|
01:05:00.816 : en:Chapter 12<br>
|
||||||
|
01:10:45.828 : en:Chapter 13<br>
|
||||||
|
01:16:38.764 : en:Chapter 14<br>
|
||||||
|
01:22:16.268 : en:Chapter 15<br>
|
||||||
|
01:29:53.809 : en:Chapter 16</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="sp-wrap">
|
||||||
|
<div class="sp-head folded"><span>Скриншоты</span></div>
|
||||||
|
<div class="sp-body"><span class="post-align" style="text-align: center;"><a href="https://postimg.cc/N548Lpj5" class="postLink p-ext-link"><var class="postImg" title="https://i.postimg.cc/N548Lpj5/mpv-shot0001.png">
|
||||||
|
</var></a> <a href="https://postimg.cc/ppZQyqLT" class="postLink p-ext-link"><var class="postImg" title="https://i.postimg.cc/ppZQyqLT/mpv-shot0002.png">
|
||||||
|
</var></a> <a href="https://postimg.cc/vcz7D3ZY" class="postLink p-ext-link"><var class="postImg" title="https://i.postimg.cc/vcz7D3ZY/mpv-shot0003.png">
|
||||||
|
</var></a> <a href="https://postimg.cc/QHbgV0dh" class="postLink p-ext-link"><var class="postImg" title="https://i.postimg.cc/QHbgV0dh/mpv-shot0004.png">
|
||||||
|
</var></a> <a href="https://postimg.cc/FYHyS3Yf" class="postLink p-ext-link"><var class="postImg" title="https://i.postimg.cc/FYHyS3Yf/mpv-shot0005.png">
|
||||||
|
</var></a> <a href="https://postimg.cc/sMgYW7MM" class="postLink p-ext-link"><var class="postImg" title="https://i.postimg.cc/sMgYW7MM/mpv-shot0006.png">
|
||||||
|
</var></a></span></div>
|
||||||
|
</div>
|
||||||
|
<div class="sp-wrap">
|
||||||
|
<div class="sp-head folded"><span>Дополнительно</span></div>
|
||||||
|
<div class="sp-body">Данный вид релизов в MKV сделан в 8-м профиле Dolby Vision для воспроизведении на устройствах с поддержкой DV. Протестировано на тв-боксах Nvidia Shield 2019, Zidoo Z9X, Ugoos AM6+, TiVo Stream 4K, Amazon 4k Stick Max и телевизорах Sony XG/XH и Phillips. На устройствах без поддержки Dolby Vision будет играть просто HDR10.<br>
|
||||||
|
Программные плееры для воспроизведения в DV(на поддерживаемых устройствах): Vimu Player 8.8.8(и выше), Kodi 19(специальная сборка с поддержкой DV), ExoPlayer, JustPlayer.<span class="post-br"><br></span>Исходник: The Super Mario Galaxy Movie 2026 Hybrid 2160p MA WEB-DL DD+ 5 1 Atmos DV HDR H 265-HONE</div>
|
||||||
|
</div> </div><!--/post_body-->
|
||||||
|
|
||||||
|
|
||||||
|
<div class="clear" style="height: 8px;"></div>
|
||||||
|
|
||||||
|
|
||||||
|
<fieldset class="attach">
|
||||||
|
<legend>Download</legend>
|
||||||
|
<div class="attach_link guest">
|
||||||
|
<ul class="inlined middot-separated">
|
||||||
|
<li>
|
||||||
|
<a href="magnet:?xt=urn:btih:92861CAAE74738654552427ADB9C57D4DE9C60DB&tr=http%3A%2F%2Fbt2.t-ru.org%2Fann%3Fmagnet&dn=%D0%A1%D1%83%D0%BF%D0%B5%D1%80%20%D0%9C%D0%B0%D1%80%D0%B8%D0%BE%3A%20%D0%93%D0%B0%D0%BB%D0%B0%D0%BA%D1%82%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%BE%D0%B5%20%D0%BA%D0%B8%D0%BD%D0%BE%20%2F%20The%20Super%20Mario%20Galaxy%20Movie%20(%D0%90%D0%B0%D1%80%D0%BE%D0%BD%20%D0%A5%D0%BE%D1%80%D0%B2%D0%B0%D1%82%20%2F%20Aaron%20Horvath)%20%5B2026%2C%20%D0%AF%D0%BF%D0%BE%D0%BD%D0%B8%D1%8F%2C%20%D0%A1%D0%A8%D0%90%2C%20%D0%BC%D1%83%D0%BB%D1%8C%D1%82%D1%84%D0%B8%D0%BB%D1%8C%D0%BC%2C%20%D1%84%D1%8D%D0%BD%D1%82%D0%B5%D0%B7%D0%B8%2C%20WEB-DL%202160p%2C%20H" class="magnet-link" data-topic_id="6870900">
|
||||||
|
<img src="https://static.rutracker.cc/templates/v1/images/magnet_1.svg" alt="">Скачать по magnet-ссылке</a>
|
||||||
|
</li>
|
||||||
|
<li>19.19 GB</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="attachment-disclaimer">
|
||||||
|
Сайт не распространяет и не хранит электронные версии произведений, а лишь предоставляет доступ к создаваемому
|
||||||
|
пользователями каталогу ссылок на <a href="https://ru.wikipedia.org/wiki/BitTorrent" target="_blank">торрент-файлы</a>,
|
||||||
|
которые содержат только списки хеш-сумм
|
||||||
|
</div>
|
||||||
|
<div class="attachment-disclaimer">
|
||||||
|
<a href="/go/1" target="_blank"><b>Как скачивать?</b></a>
|
||||||
|
<i>(для скачивания <b>.torrent</b> файлов необходима
|
||||||
|
<a href="profile.php?mode=register" target="_blank"><b>регистрация</b></a>)</i>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</div><!--/post_wrap-->
|
||||||
|
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="poster_btn td3 hide-for-print">
|
||||||
|
|
||||||
|
<div style="padding: 2px 6px 4px;" class="post_btn_2">
|
||||||
|
<a class="txtb" href="profile.php?mode=viewprofile&u=13870228">[Профиль]</a>
|
||||||
|
<a class="txtb" href="privmsg.php?mode=post&u=13870228">[ЛС]</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<tbody id="post_89371201" class="row2">
|
||||||
|
<tr>
|
||||||
|
<td class="poster_info td1 hide-for-print">
|
||||||
|
<a id="89371201"></a>
|
||||||
|
|
||||||
|
|
||||||
|
<p class="nick ">Mr_Korzh</p>
|
||||||
|
|
||||||
|
|
||||||
|
<p class="avatar"><img src="https://static.rutracker.cc/avatars/1/54/7901954.jpg" alt=""></p> <p class="joined"><em>Стаж:</em> 17 лет 9 месяцев</p> <p class="posts"><em>Сообщений:</em> 270</p>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td class="message td2" rowspan="2">
|
||||||
|
|
||||||
|
<div class="post_head">
|
||||||
|
<p class="post-time">
|
||||||
|
<span class="hl-scrolled-to-wrap">
|
||||||
|
<span class="show-for-print bold">Mr_Korzh · </span>
|
||||||
|
<img src="https://static.rutracker.cc/templates/v1/images/icon_minipost.gif" class="icon1 hide-for-print" alt="">
|
||||||
|
<a class="p-link small" href="viewtopic.php?p=89371201#89371201">14-Июл-26 17:13</a>
|
||||||
|
</span>
|
||||||
|
<span class="posted_since hide-for-print">(спустя 29 дней)</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul class="inlined t-post-buttons hide-for-print" style="float: right; padding: 3px 2px 4px;">
|
||||||
|
|
||||||
|
<li><a class="txtb" href="posting.php?mode=quote&p=89371201">[Цитировать]</a></li>
|
||||||
|
</ul>
|
||||||
|
<div class="clear"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="post_wrap" id="p-20862-2">
|
||||||
|
<div class="post_body" id="p-89371201" data-ext_link_data="{"p":89371201,"t":6870900,"f":4,"u":7901954}">
|
||||||
|
Прошу прощения. А дубляж от RHS планируется добавить в релиз? И если да, то когда? </div><!--/post_body-->
|
||||||
|
|
||||||
|
|
||||||
|
</div><!--/post_wrap-->
|
||||||
|
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="poster_btn td3 hide-for-print">
|
||||||
|
|
||||||
|
<div style="padding: 2px 6px 4px;" class="post_btn_2">
|
||||||
|
<a class="txtb" href="profile.php?mode=viewprofile&u=7901954">[Профиль]</a>
|
||||||
|
<a class="txtb" href="privmsg.php?mode=post&u=7901954">[ЛС]</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</table><!--/topic_main-->
|
||||||
|
|
||||||
|
<script>$('img.smile').remove();</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<table class="topic hide-for-print">
|
||||||
|
<tbody><tr>
|
||||||
|
<td class="catBottom med">
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody></table>
|
||||||
|
|
||||||
|
<table class="w100 hide-for-print" style="padding-top: 2px;">
|
||||||
|
<tbody><tr>
|
||||||
|
<td class="vTop">
|
||||||
|
<a href="posting.php?mode=reply&t=6870900"><img src="https://static.rutracker.cc/templates/v1/images/reply.gif" alt="Ответить"></a>
|
||||||
|
</td>
|
||||||
|
<td class="nav w100" style="padding-left: 8px;">
|
||||||
|
<a href="index.php">Главная</a>
|
||||||
|
<em>»</em> <a href="index.php?c=2">Кино, Видео и ТВ</a>
|
||||||
|
<em>»</em> <a href="viewforum.php?f=4">Мультфильмы</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody></table>
|
||||||
|
|
||||||
|
<div class="bottom_info hide-for-print">
|
||||||
|
|
||||||
|
<div class="tRight">
|
||||||
|
<span id="jumpbox-wrap"><select>,<option style="width: 250px;"> »» Выберите форум для перехода </option>,</select></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table class="w100">
|
||||||
|
<tbody><tr>
|
||||||
|
<td class="tRight med vTop">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody></table>
|
||||||
|
|
||||||
|
</div><!--/bottom_info-->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</div><!--/main_content_wrap-->
|
||||||
|
</td><!--/main_content-->
|
||||||
|
|
||||||
|
</tr>
|
||||||
|
</tbody></table>
|
||||||
|
</div><!--/page_content-->
|
||||||
|
|
||||||
|
<!--page_footer-->
|
||||||
|
<div id="page_footer" class="hide-for-print">
|
||||||
|
|
||||||
|
|
||||||
|
<div class="clear"></div>
|
||||||
|
|
||||||
|
<nav id="footer-info-links">
|
||||||
|
<div>
|
||||||
|
<a href="info.php?show=user_agreement" rel="nofollow">Условия использования</a>
|
||||||
|
·
|
||||||
|
<a href="info.php?show=advert" rel="nofollow">Реклама на сайте</a>
|
||||||
|
·
|
||||||
|
<a href="info.php?show=copyright_holders" rel="nofollow">Для правообладателей</a>
|
||||||
|
·
|
||||||
|
<a href="info.php?show=press" rel="nofollow">Для прессы</a>
|
||||||
|
·
|
||||||
|
<a href="viewtopic.php?t=2234744">Для провайдеров</a>
|
||||||
|
·
|
||||||
|
<a href="https://rutracker.wiki">Торрентопедия</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="viewtopic.php?t=1045"><b>Правила</b></a>
|
||||||
|
·
|
||||||
|
<a href="/go/8" rel="nofollow">Кому задать вопрос</a>
|
||||||
|
·
|
||||||
|
<a href="viewforum.php?f=1538">Авторские раздачи</a>
|
||||||
|
·
|
||||||
|
<a href="viewforum.php?f=1289">Конкурсы</a>
|
||||||
|
·
|
||||||
|
<a href="viewforum.php?f=2332">Новости "Хранителей" и "Антикваров"</a>
|
||||||
|
·
|
||||||
|
<a href="#" onclick="return post2url('index.php', {rand_rel: 1});" rel="nofollow">Случайная раздача</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="footer-bottom-links">
|
||||||
|
<ul class="inlined middot-separated">
|
||||||
|
<li><a href="groupcp.php?g=104792" rel="nofollow" target="_blank">Администрация</a></li>
|
||||||
|
<li><a href="groupcp.php?g=104787" rel="nofollow" target="_blank">Модераторы</a></li>
|
||||||
|
<li><a href="groupcp.php?g=104841" rel="nofollow" target="_blank">Тех. помощь</a></li>
|
||||||
|
<li><a href="https://t.me/rutracker_news" rel="nofollow" target="_blank">Telegram-канал</a></li>
|
||||||
|
</ul>
|
||||||
|
<div class="wide">
|
||||||
|
<script>
|
||||||
|
$(function () {
|
||||||
|
var title = "";
|
||||||
|
var src = "https://counter.yadro.ru/hit?t44.1;r" + encodeURIComponent(document.referrer) +
|
||||||
|
((typeof (screen) == "undefined") ? "" : ";s" + screen.width + "*" + screen.height + "*" + (screen.colorDepth ? screen.colorDepth : screen.pixelDepth)) +
|
||||||
|
";u" + encodeURIComponent(document.URL) + ";h" + encodeURIComponent(title.substring(0, 80)) + ";" + Math.random();
|
||||||
|
$('<img src="' + src + '" alt="">').on('load', function () {
|
||||||
|
$('#liveinternet').append(this);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<a id="liveinternet" href="https://www.liveinternet.ru/stat/rutracker.org/index.html?lang=ru" target="_blank"></a>
|
||||||
|
</div>
|
||||||
|
<ul class="inlined middot-separated">
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div><!--/page_footer-->
|
||||||
|
</div><!--/page_container-->
|
||||||
|
|
||||||
|
|
||||||
|
<div id="ajax-loading"><b>Loading...</b></div>
|
||||||
|
<div id="ajax-error"><b>Error</b></div>
|
||||||
|
<style>
|
||||||
|
#bb-alert-box {
|
||||||
|
width: auto;
|
||||||
|
max-width: 800px;
|
||||||
|
line-height: 18px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
#bb-alert-msg {
|
||||||
|
min-width: 400px;
|
||||||
|
max-height: 400px;
|
||||||
|
margin: 50px 20px;
|
||||||
|
padding: 10px;
|
||||||
|
overflow: auto;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.bb-alert-err {
|
||||||
|
color: #7E0000;
|
||||||
|
background: #FFEEEE;
|
||||||
|
box-shadow: 0 0 20px #B85353;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div id="bb-alert-box">
|
||||||
|
<div id="bb-alert-msg"></div>
|
||||||
|
</div>
|
||||||
|
<div id="modal-blocker"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
$(function () {
|
||||||
|
if (!BB.hasET() && BB.getUrlParam('view') !== 'print') {
|
||||||
|
BB.navbar.init();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<div id="nav-panel" class="nav-hidden-arrow hide-for-print" style="display: block;">
|
||||||
|
<div id="nav-up" class="nav-btn" title="Вверх">
|
||||||
|
<span class="nav-icon"></span>
|
||||||
|
</div>
|
||||||
|
<div id="nav-settings" class="nav-btn">
|
||||||
|
<span class="nav-icon" title="Настройки"></span>
|
||||||
|
<ul id="nav-opt-menu">
|
||||||
|
<li class="nav-event" data-event="click">Показывать по клику</li>
|
||||||
|
<li class="nav-event" data-event="mouseenter">Показывать по наведению</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div id="nav-down" class="nav-btn" title="Вниз">
|
||||||
|
<span class="nav-icon"></span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-hidden-overlay"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div><!--/body_container-->
|
||||||
|
<script>(function(){function c(){var b=a.contentDocument||(a.contentWindow&&a.contentWindow.document);if(b){var d=b.createElement('script');d.innerHTML="window.__CF$cv$params={r:'a5112be10c7d6a8c',t:'MTc5MzA5ODU1Nw=='};var a=document.createElement('script');a.src='/cdn-cgi/challenge-platform/scripts/jsd/main.js';document.getElementsByTagName('head')[0].appendChild(a);";b.getElementsByTagName('head')[0].appendChild(d)}}if(document.body){var a=document.createElement('iframe');a.height=1;a.width=1;a.style.position='absolute';a.style.top=0;a.style.left=0;a.style.border='none';a.style.visibility='hidden';document.body.appendChild(a);if('loading'!==document.readyState)c();else if(window.addEventListener)document.addEventListener('DOMContentLoaded',c);else{var e=document.onreadystatechange||function(){};document.onreadystatechange=function(b){e(b);'loading'!==document.readyState&&(document.onreadystatechange=e,c())}}}})();</script><iframe height="1" width="1" style="position: absolute; top: 0px; left: 0px; border-width: medium; border-style: none; border-color: currentcolor; border-image: none; visibility: hidden;"></iframe><script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/vce701823e78c4301a35b7e07294d87301792088814092" integrity="sha512-UKNk8GbxkABxKcXYuLkO16EleLGUkcvJWtpSNmExE0Too3ZaFAxTscNWEc1OdAv5/BikHawapSCaiD2XCmp+Bw==" data-cf-beacon="{"version":"2024.11.0","token":"644a095bdbc6472da3072c1512cdb737","r":1,"spa":2}" crossorigin="anonymous"></script>
|
||||||
|
<div id="injected-css" style="display: none;"><style>.post_wrap { max-width: 1731px }</style></div></body></html>
|
||||||
@@ -64,3 +64,23 @@ BIO-разметка строится по символьным спанам ч
|
|||||||
|
|
||||||
Лучшая по joint exact на held-out test копируется в `models/best`.
|
Лучшая по joint exact на held-out test копируется в `models/best`.
|
||||||
Итог — в `reports/summary.md`.
|
Итог — в `reports/summary.md`.
|
||||||
|
|
||||||
|
## Сегментатор страниц (src/segmenter.py) — главный путь
|
||||||
|
|
||||||
|
Какие строки страницы описывают дорожки, решает обученная модель
|
||||||
|
(rubert-tiny2, бинарная классификация строки с контекстом «пред. || след.»),
|
||||||
|
а не регулярки. Правила (`html_extract.extract_segments`) — запасной путь:
|
||||||
|
используются, если `models/segmenter` нет или модель не нашла ни одной строки;
|
||||||
|
режим можно форсировать: `predict_release(..., segmenter="rules")`.
|
||||||
|
|
||||||
|
* Обучение: `python -m src.segmenter --train` (~1 мин). Разметка строк — по
|
||||||
|
`raw` из `labels.csv` (362 страницы в `data/pages`); синтетика: `raw` всех
|
||||||
|
train-строк (в т.ч. nnm/kinozal без страниц) вставляются в контекст реальных
|
||||||
|
строк страниц. Страницы делятся по хешу `release_id` (≈20% в тест).
|
||||||
|
* Метрики: строки (test) P 0.936 / R 0.994; e2e на непросмотренных страницах
|
||||||
|
rutracker (n=64) — precision 0.904 → 0.946 против правил, recall 0.975 → 0.975
|
||||||
|
(`tests/e2e_segmenter.py`; выборка мала, разброс большой).
|
||||||
|
* Нарезка ВНУТРИ строки («Дублированный, многоголосый») в данных не размечена
|
||||||
|
(91 группа из 6735) — остаётся на правилах `_split_entries`, но только на
|
||||||
|
строках, выбранных моделью.
|
||||||
|
* ONNX-путь (Rust/TS) сегментатор пока не включает: там по-прежнему правила.
|
||||||
@@ -107,6 +107,24 @@ $b = [Text.Encoding]::UTF8.GetBytes((@{ text = "Перевод: Дублиров
|
|||||||
Ответ сервис отдаёт с `charset=utf-8`, поэтому кириллица в ответе
|
Ответ сервис отдаёт с `charset=utf-8`, поэтому кириллица в ответе
|
||||||
корректно декодируется и в PS 5.1.
|
корректно декодируется и в PS 5.1.
|
||||||
|
|
||||||
|
### Cloudflare / 403 на трекерах (FlareSolverr)
|
||||||
|
|
||||||
|
Как и Jackett/Prowlarr, сервис умеет ходить через **ваш** экземпляр
|
||||||
|
[FlareSolverr](https://github.com/FlareSolverr/FlareSolverr): при ответе трекера
|
||||||
|
403/429/503 или странице-заглушке («Один момент…») запрос повторяется через него.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d --name flaresolverr -p 8191:8191 ghcr.io/flaresolverr/flaresolverr:latest
|
||||||
|
python -m src.service --flaresolverr http://127.0.0.1:8191 # или env DUB_FLARESOLVERR_URL
|
||||||
|
python -m src.predict --url <ссылка> --flaresolverr http://127.0.0.1:8191
|
||||||
|
```
|
||||||
|
|
||||||
|
Адрес FlareSolverr задаётся только настройкой сервиса, не полем запроса.
|
||||||
|
Если grabber уже работает через Prowlarr/FlareSolverr, проще скачать страницу
|
||||||
|
там и прислать её в поле `html`. Ограничения: FlareSolverr не гарантирует
|
||||||
|
обход (rutracker дополнительно требует авторизацию / может блокировать по
|
||||||
|
региону), cookie сессии передаются через `cookie`.
|
||||||
|
|
||||||
### grabber (PHP)
|
### grabber (PHP)
|
||||||
|
|
||||||
kinozal требует авторизации, поэтому grabber скачивает страницу сам
|
kinozal требует авторизации, поэтому grabber скачивает страницу сам
|
||||||
|
|||||||
@@ -93,3 +93,11 @@
|
|||||||
|
|
||||||
Для e2e есть страницы только старых релизов (`data/pages`), для новых строк их нет.
|
Для e2e есть страницы только старых релизов (`data/pages`), для новых строк их нет.
|
||||||
Если порог 99% по спану критичен — для встраивания брать fp32 tiny2 (116 МБ).
|
Если порог 99% по спану критичен — для встраивания брать fp32 tiny2 (116 МБ).
|
||||||
|
|
||||||
|
## Сегментация страницы: правила, а не нейросеть
|
||||||
|
|
||||||
|
Нейромодель получает уже вырезанный текст одной дорожки. Вырезает его
|
||||||
|
`src/html_extract.py` — регулярками («Перевод N:», разделители, запятые).
|
||||||
|
Поэтому ошибки вида «Дубляж, Продубляж» режется на две дорожки — это ошибки правил.
|
||||||
|
Следующий шаг (не сделан): токен-классификатор по строкам страницы, который
|
||||||
|
сам находит границы дорожек.
|
||||||
@@ -3,6 +3,8 @@
|
|||||||
# Примеры (из корня проекта):
|
# Примеры (из корня проекта):
|
||||||
# powershell -NoProfile -ExecutionPolicy Bypass -File examples\powershell\predict.ps1 -Text "Перевод: Дублированный | MovieDalen"
|
# powershell -NoProfile -ExecutionPolicy Bypass -File examples\powershell\predict.ps1 -Text "Перевод: Дублированный | MovieDalen"
|
||||||
# powershell -NoProfile -ExecutionPolicy Bypass -File examples\powershell\predict.ps1 -HtmlFile page.html
|
# powershell -NoProfile -ExecutionPolicy Bypass -File examples\powershell\predict.ps1 -HtmlFile page.html
|
||||||
|
# powershell -NoProfile -ExecutionPolicy Bypass -File examples\powershell\predict.ps1 -ReleaseUrl "https://rutracker.org/forum/viewtopic.php?t=123"
|
||||||
|
# (kinozal: добавьте -Cookie "uid=..; pass=..")
|
||||||
# pwsh -File examples\powershell\predict.ps1 -Text "..." -Url http://host:8000
|
# pwsh -File examples\powershell\predict.ps1 -Text "..." -Url http://host:8000
|
||||||
#
|
#
|
||||||
# Тело запроса отправляется UTF-8 байтами — это одинаково работает и в
|
# Тело запроса отправляется UTF-8 байтами — это одинаково работает и в
|
||||||
@@ -14,7 +16,11 @@ param(
|
|||||||
[Parameter(Mandatory = $true, ParameterSetName = 'Text')]
|
[Parameter(Mandatory = $true, ParameterSetName = 'Text')]
|
||||||
[string]$Text,
|
[string]$Text,
|
||||||
[Parameter(Mandatory = $true, ParameterSetName = 'Html')]
|
[Parameter(Mandatory = $true, ParameterSetName = 'Html')]
|
||||||
[string]$HtmlFile
|
[string]$HtmlFile,
|
||||||
|
[Parameter(Mandatory = $true, ParameterSetName = 'Release')]
|
||||||
|
[string]$ReleaseUrl,
|
||||||
|
[Parameter(ParameterSetName = 'Release')]
|
||||||
|
[string]$Cookie
|
||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
@@ -29,6 +35,9 @@ if ($HtmlFile) {
|
|||||||
$html = [IO.File]::ReadAllText($path, [Text.Encoding]::GetEncoding(1251))
|
$html = [IO.File]::ReadAllText($path, [Text.Encoding]::GetEncoding(1251))
|
||||||
}
|
}
|
||||||
$payload = @{ html = $html }
|
$payload = @{ html = $html }
|
||||||
|
} elseif ($ReleaseUrl) {
|
||||||
|
$payload = @{ url = $ReleaseUrl }
|
||||||
|
if ($Cookie) { $payload.cookie = $Cookie }
|
||||||
} else {
|
} else {
|
||||||
$payload = @{ text = $Text }
|
$payload = @{ text = $Text }
|
||||||
}
|
}
|
||||||
@@ -40,10 +49,19 @@ try {
|
|||||||
$r = Invoke-RestMethod -Uri "$Url/predict" -Method Post `
|
$r = Invoke-RestMethod -Uri "$Url/predict" -Method Post `
|
||||||
-ContentType 'application/json' -Body $bytes
|
-ContentType 'application/json' -Body $bytes
|
||||||
} catch {
|
} catch {
|
||||||
|
if ($_.Exception.Response) {
|
||||||
|
# сервис ответил ошибкой (например, трекер вернул 403) — показываем его текст
|
||||||
|
$detail = $_.ErrorDetails.Message
|
||||||
|
Write-Error "Сервис вернул ошибку: $detail"
|
||||||
|
} else {
|
||||||
Write-Error "Сервис недоступен ($Url). Запуск: python -m src.service`n$_"
|
Write-Error "Сервис недоступен ($Url). Запуск: python -m src.service`n$_"
|
||||||
|
}
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($r.studios) {
|
||||||
|
"18+ релиза: $($r.is_18plus) типы: $($r.translation_types -join ', ')"
|
||||||
|
}
|
||||||
if ($r.tracks) {
|
if ($r.tracks) {
|
||||||
$r.tracks | Format-Table translation_type, studio, is_18plus,
|
$r.tracks | Format-Table translation_type, studio, is_18plus,
|
||||||
type_confidence -AutoSize
|
type_confidence -AutoSize
|
||||||
|
|||||||
+69
-8
@@ -11,10 +11,19 @@
|
|||||||
kinozal отдаёт описание только авторизованным пользователям, поэтому для него
|
kinozal отдаёт описание только авторизованным пользователям, поэтому для него
|
||||||
передавайте cookie (`cookie="uid=...; pass=..."`) либо скачивайте страницу сами
|
передавайте cookie (`cookie="uid=...; pass=..."`) либо скачивайте страницу сами
|
||||||
(grabber уже умеет) и отдавайте сервису готовый `html`.
|
(grabber уже умеет) и отдавайте сервису готовый `html`.
|
||||||
|
|
||||||
|
Cloudflare / защита от ботов: если задан FlareSolverr (ваш собственный
|
||||||
|
экземпляр, как у Jackett/Prowlarr) — `flaresolverr="http://host:8191"` или
|
||||||
|
переменная окружения DUB_FLARESOLVERR_URL, — то при ответе 403/503 или
|
||||||
|
странице-заглушке страница запрашивается через него. Адрес FlareSolverr берётся
|
||||||
|
только из настроек сервиса, но не из запроса клиента.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import urllib.error
|
||||||
import urllib.request
|
import urllib.request
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
@@ -29,6 +38,8 @@ TRACKER_HOSTS = {
|
|||||||
USER_AGENT = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
|
USER_AGENT = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
|
||||||
"(KHTML, like Gecko) Chrome/124.0 Safari/537.36")
|
"(KHTML, like Gecko) Chrome/124.0 Safari/537.36")
|
||||||
MAX_BYTES = 5_000_000
|
MAX_BYTES = 5_000_000
|
||||||
|
# коды, при которых имеет смысл повтор через FlareSolverr (отказ/Cloudflare)
|
||||||
|
RETRY_CODES = (403, 429, 500, 502, 503, 520, 521, 522, 523, 524)
|
||||||
|
|
||||||
|
|
||||||
def detect_tracker(url: str) -> str:
|
def detect_tracker(url: str) -> str:
|
||||||
@@ -63,29 +74,79 @@ def blocked_reason(html: str) -> str | None:
|
|||||||
return "Cloudflare challenge"
|
return "Cloudflare challenge"
|
||||||
if "takelogin.php" in html and "Перевод" not in html:
|
if "takelogin.php" in html and "Перевод" not in html:
|
||||||
return "нужна авторизация (передайте cookie)"
|
return "нужна авторизация (передайте cookie)"
|
||||||
if "login.php" in head and "name=\"login_username\"" in html:
|
# у rutracker форма логина есть в шапке и у обычной страницы темы
|
||||||
|
if "login.php" in head and "name=\"login_username\"" in html and "post_body" not in html and "Перевод" not in html:
|
||||||
return "нужна авторизация (передайте cookie)"
|
return "нужна авторизация (передайте cookie)"
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def fetch_html(url: str, *, timeout: float = 15.0,
|
def _fetch_direct(url: str, timeout: float, cookie: str | None) -> str:
|
||||||
cookie: str | None = None) -> tuple[str, str]:
|
|
||||||
"""Скачивает страницу релиза. Возвращает (html, tracker)."""
|
|
||||||
tracker = detect_tracker(url)
|
|
||||||
headers = {"User-Agent": USER_AGENT, "Accept-Language": "ru,en;q=0.8"}
|
headers = {"User-Agent": USER_AGENT, "Accept-Language": "ru,en;q=0.8"}
|
||||||
if cookie:
|
if cookie:
|
||||||
headers["Cookie"] = cookie
|
headers["Cookie"] = cookie
|
||||||
req = urllib.request.Request(url, headers=headers)
|
req = urllib.request.Request(url, headers=headers)
|
||||||
# редиректы на чужие хосты не нужны: открываем без follow-за-пределы allowlist
|
# редиректы только внутри allowlist трекеров
|
||||||
opener = urllib.request.build_opener(_AllowlistRedirect())
|
opener = urllib.request.build_opener(_AllowlistRedirect())
|
||||||
with opener.open(req, timeout=timeout) as resp:
|
with opener.open(req, timeout=timeout) as resp:
|
||||||
data = resp.read(MAX_BYTES + 1)
|
data = resp.read(MAX_BYTES + 1)
|
||||||
if len(data) > MAX_BYTES:
|
if len(data) > MAX_BYTES:
|
||||||
raise ValueError("страница слишком большая")
|
raise ValueError("страница слишком большая")
|
||||||
html = decode_bytes(data, resp.headers.get("Content-Type", ""))
|
return decode_bytes(data, resp.headers.get("Content-Type", ""))
|
||||||
|
|
||||||
|
|
||||||
|
def _fetch_flaresolverr(endpoint: str, url: str, timeout: float,
|
||||||
|
cookie: str | None) -> str:
|
||||||
|
"""Запрос через FlareSolverr (POST {endpoint}/v1, cmd=request.get)."""
|
||||||
|
host = urlparse(url).hostname or ""
|
||||||
|
body = {"cmd": "request.get", "url": url, "maxTimeout": int(timeout * 1000)}
|
||||||
|
if cookie:
|
||||||
|
body["cookies"] = [
|
||||||
|
{"name": k.strip(), "value": v.strip(), "domain": host}
|
||||||
|
for k, _, v in (c.partition("=") for c in cookie.split(";") if "=" in c)]
|
||||||
|
req = urllib.request.Request(
|
||||||
|
endpoint.rstrip("/") + "/v1", data=json.dumps(body).encode(),
|
||||||
|
headers={"Content-Type": "application/json"})
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req, timeout=timeout + 15) as resp:
|
||||||
|
data = json.loads(resp.read(MAX_BYTES * 2).decode("utf-8"))
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
# FlareSolverr при неудаче решения отвечает HTTP 500 с JSON-описанием
|
||||||
|
try:
|
||||||
|
msg = json.loads(e.read().decode("utf-8")).get("message", "")
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
msg = ""
|
||||||
|
raise ValueError(f"FlareSolverr: HTTP {e.code} {msg}".strip()) from e
|
||||||
|
if data.get("status") != "ok":
|
||||||
|
raise ValueError(f"FlareSolverr: {data.get('message', 'ошибка')}")
|
||||||
|
return data["solution"]["response"]
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_html(url: str, *, timeout: float = 15.0, cookie: str | None = None,
|
||||||
|
flaresolverr: str | None = None) -> tuple[str, str]:
|
||||||
|
"""Скачивает страницу релиза. Возвращает (html, tracker).
|
||||||
|
|
||||||
|
Сначала прямой запрос; при 403/503 или странице-заглушке и заданном
|
||||||
|
FlareSolverr — повтор через него.
|
||||||
|
"""
|
||||||
|
tracker = detect_tracker(url)
|
||||||
|
flaresolverr = flaresolverr or os.environ.get("DUB_FLARESOLVERR_URL") or None
|
||||||
|
html, direct_error = None, None
|
||||||
|
try:
|
||||||
|
html = _fetch_direct(url, timeout, cookie)
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
if e.code not in RETRY_CODES or not flaresolverr:
|
||||||
|
raise
|
||||||
|
direct_error = e
|
||||||
|
if html is not None and blocked_reason(html) and flaresolverr:
|
||||||
|
html = None
|
||||||
|
if html is None:
|
||||||
|
if not flaresolverr:
|
||||||
|
raise ValueError("страница недоступна")
|
||||||
|
html = _fetch_flaresolverr(flaresolverr, url, timeout, cookie)
|
||||||
reason = blocked_reason(html)
|
reason = blocked_reason(html)
|
||||||
if reason:
|
if reason:
|
||||||
raise ValueError(f"страница недоступна: {reason}")
|
hint = "" if flaresolverr else " (задайте FlareSolverr: DUB_FLARESOLVERR_URL)"
|
||||||
|
raise ValueError(f"страница недоступна: {reason}{hint}")
|
||||||
return html, tracker
|
return html, tracker
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+21
-4
@@ -40,7 +40,7 @@ RE_TYPE_HINT = re.compile(
|
|||||||
# корневые слова типов — для разбиения перечислений через запятую
|
# корневые слова типов — для разбиения перечислений через запятую
|
||||||
# корни «голосности»: «Авторский, одноголосый» — одна дорожка (авторск — лишь
|
# корни «голосности»: «Авторский, одноголосый» — одна дорожка (авторск — лишь
|
||||||
# уточнение), поэтому по запятым режем только перечисления голосностей
|
# уточнение), поэтому по запятым режем только перечисления голосностей
|
||||||
RE_TYPE_ROOT = re.compile(r"(?i)(дубл|многоголос|двухголос|одноголос)")
|
RE_TYPE_ROOT = re.compile(r"(?i)\b(дубл|многоголос|двухголос|одноголос)")
|
||||||
|
|
||||||
# разделители нескольких дорожек в одной строке (перед словом типа озвучки)
|
# разделители нескольких дорожек в одной строке (перед словом типа озвучки)
|
||||||
RE_SEP = re.compile(
|
RE_SEP = re.compile(
|
||||||
@@ -50,15 +50,32 @@ RE_SEP = re.compile(
|
|||||||
SELECTORS = ("div.post_body", "div[id^=post-text-]", ".postbody", "#tabs")
|
SELECTORS = ("div.post_body", "div[id^=post-text-]", ".postbody", "#tabs")
|
||||||
|
|
||||||
|
|
||||||
|
BLOCK_TAGS = ("p", "div", "tr", "li", "ul", "ol", "table", "hr", "h1", "h2", "h3",
|
||||||
|
"h4", "h5", "h6", "blockquote", "pre", "dt", "dd")
|
||||||
|
|
||||||
|
|
||||||
|
def _node_text(node) -> str:
|
||||||
|
"""Текст узла: перевод строки только на <br> и блочных тегах.
|
||||||
|
|
||||||
|
Инлайн-теги (<a>, <span>, <b>) строку не рвут: на rutracker имя студии —
|
||||||
|
ссылка внутри строки «Перевод 1: Профессиональный (…) <a>Студия</a>».
|
||||||
|
"""
|
||||||
|
for br in node.find_all("br"):
|
||||||
|
br.replace_with("\n")
|
||||||
|
for tag in node.find_all(BLOCK_TAGS):
|
||||||
|
tag.insert_before("\n")
|
||||||
|
tag.insert_after("\n")
|
||||||
|
return node.get_text("")
|
||||||
|
|
||||||
|
|
||||||
def html_to_text(html: str) -> str:
|
def html_to_text(html: str) -> str:
|
||||||
"""HTML -> текст: контейнер поста/вкладки описания либо весь документ."""
|
"""HTML -> текст: контейнер поста/вкладки описания либо весь документ."""
|
||||||
soup = BeautifulSoup(html, "lxml")
|
soup = BeautifulSoup(html, "lxml")
|
||||||
for sel in SELECTORS:
|
for sel in SELECTORS:
|
||||||
node = soup.select_one(sel)
|
node = soup.select_one(sel)
|
||||||
if node and node.get_text(strip=True):
|
if node and node.get_text(strip=True):
|
||||||
return node.get_text("\n")
|
return _node_text(node)
|
||||||
body = soup.body or soup
|
return _node_text(soup.body or soup)
|
||||||
return body.get_text("\n")
|
|
||||||
|
|
||||||
|
|
||||||
def _split_entries(body: str) -> list[str]:
|
def _split_entries(body: str) -> list[str]:
|
||||||
|
|||||||
+11
-2
@@ -59,7 +59,8 @@ def refine_span(text: str, span: tuple[int, int] | None) -> tuple[int, int] | No
|
|||||||
class DubPredictor:
|
class DubPredictor:
|
||||||
"""Загруженная модель для предсказания типа озвучки и автора."""
|
"""Загруженная модель для предсказания типа озвучки и автора."""
|
||||||
|
|
||||||
def __init__(self, model_dir: str | Path = "models/best", device: str | None = None):
|
def __init__(self, model_dir: str | Path = "models/best", device: str | None = None,
|
||||||
|
segmenter_dir: str | Path = "models/segmenter"):
|
||||||
self.model_dir = Path(model_dir)
|
self.model_dir = Path(model_dir)
|
||||||
if device is None:
|
if device is None:
|
||||||
device = "cuda" if torch.cuda.is_available() else "cpu"
|
device = "cuda" if torch.cuda.is_available() else "cpu"
|
||||||
@@ -68,6 +69,12 @@ class DubPredictor:
|
|||||||
self.types = maps["types"]
|
self.types = maps["types"]
|
||||||
self.tokenizer = AutoTokenizer.from_pretrained(self.model_dir)
|
self.tokenizer = AutoTokenizer.from_pretrained(self.model_dir)
|
||||||
self.model = load_model(self.model_dir, device)
|
self.model = load_model(self.model_dir, device)
|
||||||
|
# обученный сегментатор страниц (главный путь); нет модели -> правила
|
||||||
|
self.segmenter = None
|
||||||
|
seg_dir = Path(segmenter_dir)
|
||||||
|
if seg_dir.exists():
|
||||||
|
from .segmenter import LineSegmenter
|
||||||
|
self.segmenter = LineSegmenter(seg_dir, device)
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@torch.no_grad()
|
@torch.no_grad()
|
||||||
@@ -149,6 +156,7 @@ def main() -> None:
|
|||||||
src.add_argument("--file", help="путь к текстовому дампу страницы")
|
src.add_argument("--file", help="путь к текстовому дампу страницы")
|
||||||
src.add_argument("--url", help="ссылка на раздачу (rutracker / kinozal / nnm)")
|
src.add_argument("--url", help="ссылка на раздачу (rutracker / kinozal / nnm)")
|
||||||
ap.add_argument("--cookie", help="Cookie для --url (нужен kinozal)")
|
ap.add_argument("--cookie", help="Cookie для --url (нужен kinozal)")
|
||||||
|
ap.add_argument("--flaresolverr", help="адрес FlareSolverr (обход Cloudflare), напр. http://127.0.0.1:8191")
|
||||||
ap.add_argument("--out", help="записать JSON в файл вместо stdout")
|
ap.add_argument("--out", help="записать JSON в файл вместо stdout")
|
||||||
args = ap.parse_args()
|
args = ap.parse_args()
|
||||||
|
|
||||||
@@ -158,7 +166,8 @@ def main() -> None:
|
|||||||
elif args.url:
|
elif args.url:
|
||||||
from .fetch import fetch_html
|
from .fetch import fetch_html
|
||||||
from .release import predict_release
|
from .release import predict_release
|
||||||
html, tracker = fetch_html(args.url, cookie=args.cookie)
|
html, tracker = fetch_html(args.url, cookie=args.cookie,
|
||||||
|
flaresolverr=args.flaresolverr)
|
||||||
payload = {**predict_release(predictor, html=html), "tracker": tracker}
|
payload = {**predict_release(predictor, html=html), "tracker": tracker}
|
||||||
else:
|
else:
|
||||||
from .release import predict_release
|
from .release import predict_release
|
||||||
|
|||||||
+39
-3
@@ -15,6 +15,9 @@ import re
|
|||||||
|
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
|
from .html_extract import (RE_LINE_PREFIX, _split_entries, extract_segments,
|
||||||
|
html_to_text)
|
||||||
|
|
||||||
# аббревиатуры типа озвучки в заголовках раздач -> фраза, понятная модели
|
# аббревиатуры типа озвучки в заголовках раздач -> фраза, понятная модели
|
||||||
TITLE_TYPES = {
|
TITLE_TYPES = {
|
||||||
"ПМ": "Профессиональный (многоголосый закадровый)",
|
"ПМ": "Профессиональный (многоголосый закадровый)",
|
||||||
@@ -86,6 +89,10 @@ def aggregate(tracks: list[dict]) -> dict:
|
|||||||
else:
|
else:
|
||||||
merged[k] = dict(t)
|
merged[k] = dict(t)
|
||||||
uniq = list(merged.values())
|
uniq = list(merged.values())
|
||||||
|
# дорожка без студии, при наличии дорожки того же типа со студией, — как
|
||||||
|
# правило, та же дорожка в виде кодек-строки («AC3 … | MVO, AlexFilm»)
|
||||||
|
typed = {t["translation_type"] for t in uniq if t.get("studio")}
|
||||||
|
uniq = [t for t in uniq if t.get("studio") or t["translation_type"] not in typed]
|
||||||
studios = [{"studio": t["studio"], "translation_type": t["translation_type"],
|
studios = [{"studio": t["studio"], "translation_type": t["translation_type"],
|
||||||
"is_18plus": t["is_18plus"]}
|
"is_18plus": t["is_18plus"]}
|
||||||
for t in uniq if t.get("studio")]
|
for t in uniq if t.get("studio")]
|
||||||
@@ -121,17 +128,45 @@ def merge_title_tracks(tracks: list[dict], title_tracks: list[dict]) -> None:
|
|||||||
tracks.append(t)
|
tracks.append(t)
|
||||||
|
|
||||||
|
|
||||||
def predict_release(predictor, *, html: str | None = None,
|
def model_segments(segmenter, page_text: str) -> list[str]:
|
||||||
text: str | None = None, title: str | None = None) -> dict:
|
"""Дорожки страницы: строки выбирает обученный сегментатор.
|
||||||
|
|
||||||
|
Нарезка внутри строки («Дублированный, многоголосый») в данных не
|
||||||
|
размечена и пока делается правилами `_split_entries`.
|
||||||
|
"""
|
||||||
|
out = []
|
||||||
|
for line in segmenter.select_lines(page_text):
|
||||||
|
body = RE_LINE_PREFIX.sub("", line, count=1)
|
||||||
|
for entry in _split_entries(body):
|
||||||
|
entry = re.sub(r"\s+", " ", entry).strip()
|
||||||
|
if entry:
|
||||||
|
out.append(entry)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def predict_release(predictor, *, html: str | None = None, text: str | None = None,
|
||||||
|
title: str | None = None, segmenter: str = "model") -> dict:
|
||||||
"""Предсказание для релиза целиком (html или текст страницы).
|
"""Предсказание для релиза целиком (html или текст страницы).
|
||||||
|
|
||||||
|
segmenter: "model" — строки-дорожки выбирает обученный сегментатор
|
||||||
|
(если он загружен и нашёл хотя бы одну строку), иначе правила; "rules" —
|
||||||
|
только правила.
|
||||||
|
|
||||||
Заголовок раздачи используется как дополнительный источник: студии,
|
Заголовок раздачи используется как дополнительный источник: студии,
|
||||||
названные только там («ПМ (LostFilm)»), добавляются как дорожки с
|
названные только там («ПМ (LostFilm)»), добавляются как дорожки с
|
||||||
пометкой source=title.
|
пометкой source=title.
|
||||||
"""
|
"""
|
||||||
if (html is None) == (text is None):
|
if (html is None) == (text is None):
|
||||||
raise ValueError("нужно ровно одно из: html, text")
|
raise ValueError("нужно ровно одно из: html, text")
|
||||||
tracks = predictor.predict_html(html) if html is not None else predictor.predict_page(text)
|
page_text = html_to_text(html) if html is not None else text
|
||||||
|
segs_page, used = [], "rules"
|
||||||
|
seg_model = getattr(predictor, "segmenter", None)
|
||||||
|
if segmenter == "model" and seg_model is not None:
|
||||||
|
segs_page = model_segments(seg_model, page_text)
|
||||||
|
used = "model" if segs_page else "rules"
|
||||||
|
if not segs_page:
|
||||||
|
segs_page = extract_segments(page_text)
|
||||||
|
tracks = predictor._predict_batch(segs_page) if segs_page else []
|
||||||
for t in tracks:
|
for t in tracks:
|
||||||
t["source"] = "page"
|
t["source"] = "page"
|
||||||
if title is None:
|
if title is None:
|
||||||
@@ -141,4 +176,5 @@ def predict_release(predictor, *, html: str | None = None,
|
|||||||
merge_title_tracks(tracks, predictor._predict_batch(segs))
|
merge_title_tracks(tracks, predictor._predict_batch(segs))
|
||||||
result = aggregate(tracks)
|
result = aggregate(tracks)
|
||||||
result["title"] = title or None
|
result["title"] = title or None
|
||||||
|
result["segmenter"] = used
|
||||||
return result
|
return result
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
"""Обучаемый сегментатор: какие строки страницы описывают дорожки озвучки.
|
||||||
|
|
||||||
|
Главный путь выделения дорожек (правила из html_extract — запасной).
|
||||||
|
Модель — rubert-tiny2 (бинарная классификация строки). Вход — строка с
|
||||||
|
контекстом: «пред. строка [SEP] строка [SEP] след. строка».
|
||||||
|
|
||||||
|
python -m src.segmenter --train # обучение -> models/segmenter
|
||||||
|
python -m src.segmenter --eval # метрики на отложенных страницах
|
||||||
|
|
||||||
|
Python API:
|
||||||
|
seg = LineSegmenter("models/segmenter")
|
||||||
|
seg.select_lines(page_text) -> list[str] # строки-дорожки (префикс не снят)
|
||||||
|
|
||||||
|
Разметка: строка страницы положительна, если содержит `raw` строки датасета
|
||||||
|
этого релиза (или наоборот). Положительные примеры добавляются синтетически:
|
||||||
|
`raw` всех train-строк датасета (включая nnm/kinozal, у которых нет страниц)
|
||||||
|
вставляются в контекст реальных строк страниц. Нарезка ВНУТРИ строки
|
||||||
|
(«Дублированный, многоголосый») в данных не размечена и остаётся на правилах.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import csv
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import random
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import torch
|
||||||
|
from transformers import AutoModelForSequenceClassification, AutoTokenizer
|
||||||
|
|
||||||
|
from .html_extract import read_input
|
||||||
|
|
||||||
|
BACKBONE = "cointegrated/rubert-tiny2"
|
||||||
|
MAX_LEN = 160
|
||||||
|
LINE_CHARS = 220
|
||||||
|
CTX_CHARS = 70
|
||||||
|
THRESHOLD = 0.5
|
||||||
|
SEED = 7
|
||||||
|
|
||||||
|
|
||||||
|
def norm(s: str) -> str:
|
||||||
|
return re.sub(r"\s+", " ", s or "").strip()
|
||||||
|
|
||||||
|
|
||||||
|
def is_test_release(release_id: str) -> bool:
|
||||||
|
"""Детерминированное разбиение страниц: ~20% релизов уходят в тест."""
|
||||||
|
return int(hashlib.md5(str(release_id).encode()).hexdigest(), 16) % 5 == 0
|
||||||
|
|
||||||
|
|
||||||
|
def make_input(prev: str, line: str, nxt: str) -> tuple[str, str]:
|
||||||
|
"""(текст-пара для токенизатора): строка + контекст."""
|
||||||
|
return norm(line)[:LINE_CHARS], f"{norm(prev)[-CTX_CHARS:]} || {norm(nxt)[:CTX_CHARS]}"
|
||||||
|
|
||||||
|
|
||||||
|
def page_lines(text: str) -> list[str]:
|
||||||
|
return [norm(l) for l in text.splitlines() if norm(l)]
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- данные
|
||||||
|
def load_pages(pages_dir: Path = Path("data/pages")) -> dict[str, list[str]]:
|
||||||
|
return {p.stem: page_lines(read_input(p)) for p in sorted(pages_dir.glob("*.txt"))}
|
||||||
|
|
||||||
|
|
||||||
|
def load_raws(csv_path: Path = Path("data/labels.csv")) -> list[dict]:
|
||||||
|
with open(csv_path, encoding="utf-8-sig") as f:
|
||||||
|
return list(csv.DictReader(f))
|
||||||
|
|
||||||
|
|
||||||
|
def label_page(lines: list[str], raws: list[str]) -> list[int]:
|
||||||
|
"""1, если строка содержит raw (или raw содержит строку ≥ 15 символов)."""
|
||||||
|
out = []
|
||||||
|
for line in lines:
|
||||||
|
hit = any(r and (r in line or (len(line) >= 15 and line in r)) for r in raws)
|
||||||
|
out.append(int(hit))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def build_examples(split: str, rng: random.Random, synth_per_page: int = 6) -> list[tuple]:
|
||||||
|
"""Список (line, ctx, label). split: 'train' | 'test'."""
|
||||||
|
pages = load_pages()
|
||||||
|
rows = load_raws()
|
||||||
|
raws_by_rel: dict[str, list[str]] = {}
|
||||||
|
for r in rows:
|
||||||
|
raws_by_rel.setdefault(r["release_id"], []).append(norm(r["raw"]))
|
||||||
|
|
||||||
|
want_test = split == "test"
|
||||||
|
page_items = [] # (release_id, lines, labels)
|
||||||
|
for rid, lines in pages.items():
|
||||||
|
if is_test_release(rid) != want_test:
|
||||||
|
continue
|
||||||
|
page_items.append((rid, lines, label_page(lines, raws_by_rel.get(rid, []))))
|
||||||
|
|
||||||
|
# все raw train-релизов (без тестовых страниц) — материал для синтетики
|
||||||
|
test_pos = {l for rid, lines in pages.items() if is_test_release(rid)
|
||||||
|
for l, y in zip(lines, label_page(lines, raws_by_rel.get(rid, []))) if y}
|
||||||
|
pool = sorted({norm(r["raw"]) for r in rows
|
||||||
|
if r["raw"] and not is_test_release(r["release_id"])
|
||||||
|
and norm(r["raw"]) not in test_pos})
|
||||||
|
|
||||||
|
ex: list[tuple] = []
|
||||||
|
for rid, lines, labels in page_items:
|
||||||
|
for i, (line, y) in enumerate(zip(lines, labels)):
|
||||||
|
prev = lines[i - 1] if i else ""
|
||||||
|
nxt = lines[i + 1] if i + 1 < len(lines) else ""
|
||||||
|
if not y and not HINT.search(line) and rng.random() > 0.15:
|
||||||
|
continue # простые негативы прореживаем, трудные (с намёками) берём все
|
||||||
|
ex.append((*make_input(prev, line, nxt), y))
|
||||||
|
if not want_test:
|
||||||
|
negs = [l for l, y in zip(lines, labels) if not y]
|
||||||
|
for _ in range(synth_per_page if negs else 0):
|
||||||
|
raw = rng.choice(pool)
|
||||||
|
p, n = rng.choice(negs), rng.choice(negs)
|
||||||
|
ex.append((*make_input(p, raw, n), 1))
|
||||||
|
return ex
|
||||||
|
|
||||||
|
|
||||||
|
# слова, делающие негативную строку «трудной»: озвучка/аудио/субтитры/кодеки
|
||||||
|
HINT = re.compile(r"(?i)(перевод|озвуч|аудио|звук|дубл|голос|субтитр|kbps|kHz|AC3|DTS|MVO|DVO|AVO)")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- модель
|
||||||
|
class LineSegmenter:
|
||||||
|
def __init__(self, model_dir: str | Path = "models/segmenter", device: str | None = None):
|
||||||
|
self.dir = Path(model_dir)
|
||||||
|
self.device = device or ("cuda" if torch.cuda.is_available() else "cpu")
|
||||||
|
self.tok = AutoTokenizer.from_pretrained(self.dir)
|
||||||
|
self.model = AutoModelForSequenceClassification.from_pretrained(self.dir).to(self.device).eval()
|
||||||
|
|
||||||
|
@torch.no_grad()
|
||||||
|
def score(self, lines: list[str]) -> np.ndarray:
|
||||||
|
"""Вероятность «строка описывает дорожку» для каждой строки."""
|
||||||
|
pairs = [make_input(lines[i - 1] if i else "", l, lines[i + 1] if i + 1 < len(lines) else "")
|
||||||
|
for i, l in enumerate(lines)]
|
||||||
|
out = []
|
||||||
|
for s in range(0, len(pairs), 128):
|
||||||
|
chunk = pairs[s:s + 128]
|
||||||
|
enc = self.tok([a for a, _ in chunk], [b for _, b in chunk], truncation="only_second",
|
||||||
|
max_length=MAX_LEN, padding=True, return_tensors="pt").to(self.device)
|
||||||
|
out.append(torch.softmax(self.model(**enc).logits, -1)[:, 1].cpu().numpy())
|
||||||
|
return np.concatenate(out) if out else np.zeros(0)
|
||||||
|
|
||||||
|
def select_lines(self, page_text: str, threshold: float = THRESHOLD) -> list[str]:
|
||||||
|
lines = page_lines(page_text)
|
||||||
|
if not lines:
|
||||||
|
return []
|
||||||
|
probs = self.score(lines)
|
||||||
|
return [l for l, p in zip(lines, probs) if p >= threshold]
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- обучение
|
||||||
|
def train(out_dir: str = "models/segmenter", epochs: int = 4, batch: int = 32, lr: float = 5e-5):
|
||||||
|
rng = random.Random(SEED)
|
||||||
|
torch.manual_seed(SEED)
|
||||||
|
tr = build_examples("train", rng)
|
||||||
|
te = build_examples("test", rng)
|
||||||
|
print(f"train {len(tr)} (pos {sum(y for *_, y in tr)}), test {len(te)} (pos {sum(y for *_, y in te)})")
|
||||||
|
device = "cuda" if torch.cuda.is_available() else "cpu"
|
||||||
|
tok = AutoTokenizer.from_pretrained(BACKBONE)
|
||||||
|
model = AutoModelForSequenceClassification.from_pretrained(BACKBONE, num_labels=2).to(device)
|
||||||
|
opt = torch.optim.AdamW(model.parameters(), lr=lr, weight_decay=0.01)
|
||||||
|
steps = epochs * ((len(tr) + batch - 1) // batch)
|
||||||
|
sched = torch.optim.lr_scheduler.OneCycleLR(opt, max_lr=lr, total_steps=steps, pct_start=0.1)
|
||||||
|
|
||||||
|
def enc(b):
|
||||||
|
return tok([x[0] for x in b], [x[1] for x in b], truncation="only_second",
|
||||||
|
max_length=MAX_LEN, padding=True, return_tensors="pt").to(device)
|
||||||
|
|
||||||
|
def evaluate(data):
|
||||||
|
model.eval()
|
||||||
|
pred = []
|
||||||
|
with torch.no_grad():
|
||||||
|
for s in range(0, len(data), 128):
|
||||||
|
pred += model(**enc(data[s:s + 128])).logits.argmax(-1).tolist()
|
||||||
|
y = [x[2] for x in data]
|
||||||
|
tp = sum(p and t for p, t in zip(pred, y))
|
||||||
|
prec = tp / max(sum(pred), 1)
|
||||||
|
rec = tp / max(sum(y), 1)
|
||||||
|
model.train()
|
||||||
|
return prec, rec
|
||||||
|
|
||||||
|
model.train()
|
||||||
|
for ep in range(epochs):
|
||||||
|
rng.shuffle(tr)
|
||||||
|
for s in range(0, len(tr), batch):
|
||||||
|
b = tr[s:s + batch]
|
||||||
|
loss = model(**enc(b), labels=torch.tensor([x[2] for x in b], device=device)).loss
|
||||||
|
loss.backward()
|
||||||
|
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
|
||||||
|
opt.step()
|
||||||
|
sched.step()
|
||||||
|
opt.zero_grad()
|
||||||
|
p, r = evaluate(te)
|
||||||
|
print(f"epoch {ep + 1}: test line precision {p:.3f} recall {r:.3f}")
|
||||||
|
Path(out_dir).mkdir(parents=True, exist_ok=True)
|
||||||
|
model.save_pretrained(out_dir)
|
||||||
|
tok.save_pretrained(out_dir)
|
||||||
|
return p, r
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||||
|
ap.add_argument("--train", action="store_true")
|
||||||
|
ap.add_argument("--out", default="models/segmenter")
|
||||||
|
ap.add_argument("--epochs", type=int, default=4)
|
||||||
|
args = ap.parse_args()
|
||||||
|
if args.train:
|
||||||
|
train(args.out, args.epochs)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -85,9 +85,15 @@ def main() -> None:
|
|||||||
ap.add_argument("--model", default="models/best")
|
ap.add_argument("--model", default="models/best")
|
||||||
ap.add_argument("--host", default="127.0.0.1")
|
ap.add_argument("--host", default="127.0.0.1")
|
||||||
ap.add_argument("--port", type=int, default=8000)
|
ap.add_argument("--port", type=int, default=8000)
|
||||||
|
ap.add_argument("--flaresolverr", default=None,
|
||||||
|
help="адрес FlareSolverr для обхода Cloudflare (или env DUB_FLARESOLVERR_URL)")
|
||||||
args = ap.parse_args()
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
import uvicorn
|
import uvicorn
|
||||||
|
if args.flaresolverr:
|
||||||
|
os.environ["DUB_FLARESOLVERR_URL"] = args.flaresolverr
|
||||||
predictor = DubPredictor(args.model)
|
predictor = DubPredictor(args.model)
|
||||||
uvicorn.run(create_app(predictor), host=args.host, port=args.port)
|
uvicorn.run(create_app(predictor), host=args.host, port=args.port)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
"""Сравнение сегментации: правила vs обученный сегментатор (predict_release).
|
||||||
|
|
||||||
|
Метрика — пары (тип, автор) на уровне релиза против меток датасета, только по
|
||||||
|
страницам, которых сегментатор не видел при обучении (is_test_release).
|
||||||
|
Запуск: .venv/Scripts/python tests/e2e_segmenter.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from collections import defaultdict
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||||
|
|
||||||
|
from src.html_extract import read_input # noqa: E402
|
||||||
|
from src.predict import DubPredictor # noqa: E402
|
||||||
|
from src.release import predict_release # noqa: E402
|
||||||
|
from src.segmenter import is_test_release # noqa: E402
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
|
||||||
|
def run(predictor, rows, mode: str, test_only: bool) -> dict:
|
||||||
|
by_rel = defaultdict(list)
|
||||||
|
for r in rows:
|
||||||
|
by_rel[r["release_id"]].append(r)
|
||||||
|
per_src = defaultdict(list)
|
||||||
|
for page in sorted((ROOT / "data" / "pages").glob("*.txt")):
|
||||||
|
rid = page.stem
|
||||||
|
if is_test_release(rid) != test_only or rid not in by_rel:
|
||||||
|
continue
|
||||||
|
gold = {(r["translation_type"], r["studio"] or None) for r in by_rel[rid]}
|
||||||
|
res = predict_release(predictor, text=read_input(page), segmenter=mode)
|
||||||
|
pred = {(t["translation_type"], t["studio"]) for t in res["tracks"]}
|
||||||
|
tp = len(gold & pred)
|
||||||
|
p = tp / len(pred) if pred else 0.0
|
||||||
|
r = tp / len(gold) if gold else 0.0
|
||||||
|
per_src[by_rel[rid][0]["source"]].append((p, r))
|
||||||
|
out = {}
|
||||||
|
for s, v in per_src.items():
|
||||||
|
out[s] = (len(v), sum(x[0] for x in v) / len(v), sum(x[1] for x in v) / len(v))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
rows = json.loads((ROOT / "data" / "processed" / "dataset.json").read_text(encoding="utf-8"))
|
||||||
|
predictor = DubPredictor()
|
||||||
|
if predictor.segmenter is None:
|
||||||
|
sys.exit("нет models/segmenter: python -m src.segmenter --train")
|
||||||
|
for test_only, name in ((True, "тестовые страницы (не виделись сегментатору)"),
|
||||||
|
(False, "train-страницы (для справки, оптимистично)")):
|
||||||
|
print(f"== {name} ==")
|
||||||
|
for mode in ("rules", "model"):
|
||||||
|
res = run(predictor, rows, mode, test_only)
|
||||||
|
print(f" {mode:5s}: " + "; ".join(
|
||||||
|
f"{s} n={n} P={p:.3f} R={r:.3f}" for s, (n, p, r) in res.items()))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -236,5 +236,84 @@ _t5 = "Русский MVO HDRezka Studio"
|
|||||||
check("span: расширение до границ слова", _t5[slice(*refine_span(_t5, (12, 14)))] == "HDRezka")
|
check("span: расширение до границ слова", _t5[slice(*refine_span(_t5, (12, 14)))] == "HDRezka")
|
||||||
check("span: один символ -> None", refine_span("М. Чадов", (0, 1)) is None)
|
check("span: один символ -> None", refine_span("М. Чадов", (0, 1)) is None)
|
||||||
|
|
||||||
|
import json as _json # noqa: E402
|
||||||
|
import threading # noqa: E402
|
||||||
|
import urllib.error # noqa: E402
|
||||||
|
from http.server import BaseHTTPRequestHandler, HTTPServer # noqa: E402
|
||||||
|
|
||||||
|
import src.fetch as _fetch # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeFlare(BaseHTTPRequestHandler):
|
||||||
|
def do_POST(self):
|
||||||
|
req = _json.loads(self.rfile.read(int(self.headers["Content-Length"])))
|
||||||
|
ok = req.get("cmd") == "request.get" and "rutracker.org" in req.get("url", "")
|
||||||
|
body = _json.dumps({"status": "ok", "solution": {"response": "<html>Перевод: Дублированный | MovieDalen</html>"}}
|
||||||
|
if ok else {"status": "error", "message": "bad"}).encode()
|
||||||
|
self.send_response(200)
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(body)
|
||||||
|
|
||||||
|
def log_message(self, *a):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
_srv = HTTPServer(("127.0.0.1", 0), _FakeFlare)
|
||||||
|
threading.Thread(target=_srv.serve_forever, daemon=True).start()
|
||||||
|
_flare = f"http://127.0.0.1:{_srv.server_port}"
|
||||||
|
|
||||||
|
|
||||||
|
def _deny(url, timeout, cookie):
|
||||||
|
raise urllib.error.HTTPError(url, 403, "Forbidden", {}, None)
|
||||||
|
|
||||||
|
|
||||||
|
_orig = _fetch._fetch_direct
|
||||||
|
_fetch._fetch_direct = _deny
|
||||||
|
try:
|
||||||
|
h, tr = _fetch.fetch_html("https://rutracker.org/forum/viewtopic.php?t=1", flaresolverr=_flare)
|
||||||
|
check("FlareSolverr: fallback при 403", "MovieDalen" in h and tr == "rutracker")
|
||||||
|
try:
|
||||||
|
_fetch.fetch_html("https://rutracker.org/forum/viewtopic.php?t=1")
|
||||||
|
check("без FlareSolverr 403 пробрасывается", False)
|
||||||
|
except urllib.error.HTTPError:
|
||||||
|
check("без FlareSolverr 403 пробрасывается", True)
|
||||||
|
finally:
|
||||||
|
_fetch._fetch_direct = _orig
|
||||||
|
_srv.shutdown()
|
||||||
|
|
||||||
|
|
||||||
|
check("rutracker: форма логина в шапке не блокирует тему",
|
||||||
|
blocked_reason('<html><a href="login.php"></a><input name="login_username"><div class="post_body">x</div></html>') is None)
|
||||||
|
check("rutracker: голая страница логина блокируется",
|
||||||
|
blocked_reason('<html><a href="login.php"></a><input name="login_username"></html>') is not None)
|
||||||
|
|
||||||
|
_rt = ('<div class="post_body"><span class="post-b">Перевод 1</span>: Профессиональный '
|
||||||
|
'(дублированный) <a href="#">Продубляж</a><br><span>Перевод 2</span>: Профессиональный '
|
||||||
|
'(многоголосый закадровый) <a>AlexFilm</a><br>Субтитры: Русские</div>')
|
||||||
|
check("html: студия-ссылка внутри строки не рвёт дорожку",
|
||||||
|
extract_segments(html_to_text(_rt)) == ["Профессиональный (дублированный) Продубляж",
|
||||||
|
"Профессиональный (многоголосый закадровый) AlexFilm"])
|
||||||
|
check("«Дубляж, Продубляж» не режется",
|
||||||
|
len(_split_entries("Russian AC3 |Дубляж, Продубляж|")) == 1)
|
||||||
|
|
||||||
|
_agg2 = aggregate([_t("проф. дубляж", "Продубляж"), _t("проф. дубляж", None),
|
||||||
|
_t("проф. двухголосый", None)])
|
||||||
|
check("aggregate: дорожка без студии того же типа схлопывается",
|
||||||
|
[x["translation_type"] for x in _agg2["tracks"]] == ["проф. дубляж", "проф. двухголосый"])
|
||||||
|
|
||||||
|
print("== сегментатор (модель) ==")
|
||||||
|
try:
|
||||||
|
from src.segmenter import LineSegmenter, is_test_release # noqa: E402
|
||||||
|
_seg = LineSegmenter("models/segmenter", "cpu")
|
||||||
|
_fx = open("data/samples/rt-topic-6870900.html", encoding="utf-8").read()
|
||||||
|
_lines = _seg.select_lines(html_to_text(_fx))
|
||||||
|
check("сегментатор: найдены 4 строки «Перевод N» реальной страницы",
|
||||||
|
sum(l.startswith("Перевод") for l in _lines) == 4, str(_lines[:5]))
|
||||||
|
check("сегментатор: «Субтитры» и «Оригинальная аудиодорожка» не дорожки",
|
||||||
|
not any(l.startswith(("Субтитры", "Оригинальная")) for l in _lines))
|
||||||
|
check("разбиение страниц детерминировано", is_test_release("6870900") == is_test_release("6870900"))
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
print(f" пропущено (нет models/segmenter): {e}")
|
||||||
|
|
||||||
print(f"\nитог: {PASS} ok, {FAIL} failed")
|
print(f"\nитог: {PASS} ok, {FAIL} failed")
|
||||||
sys.exit(1 if FAIL else 0)
|
sys.exit(1 if FAIL else 0)
|
||||||
Reference in new issue
Block a user