PHP code example of srt / translation-validator

1. Go to this page and download the library: Download srt/translation-validator library. Choose the download type require.

2. Extract the ZIP file and open the index.php.

3. Add this code to the index.php.
    
        
<?php
require_once('vendor/autoload.php');

/* Start to develop here. Best regards https://php-download.com/ */

    

srt / translation-validator example snippets




rtValidator\SrtTranslationValidator;

$validator = new SrtTranslationValidator();

// Tune timestamp drift tolerance (default is 0.5 seconds)
$validator->setTimestampTolerance(0.2);

// Fail on any error-severity defect instead of judging by ratios
$validator->setStrict(true);

// Override any quality threshold
// (defaults: loss 0.01 / drift 0.02 / merge 0.10 /
//  verbatim 0.50 / near-verbatim 0.50 / script 0.00;
//  partial_translation is advisory)
$validator->setMaxLossRatio(0.02);
$validator->setMaxDriftRatio(0.05);
$validator->setMaxMergeRatio(0.20);
$validator->setMaxVerbatimRatio(0.60);
$validator->setMaxNearVerbatimRatio(0.60);
$validator->setMaxScriptRatio(0.001);

// Declare the source language (e.g. from the job request): when it
// matches the target, the verbatim/untranslated gates are skipped
// (same-language passthrough). For non-Latin targets this is also
// inferred from the source script automatically.
$validator->setSourceLanguage('en');

// Fail when more than N error-severity defects are found,
// regardless of the ratios
$validator->setMaxErrors(10);

$result = $validator->validate(
    'path/to/original.srt',   // reference subtitles
    'path/to/translation.srt', // the translation being checked
    'de'                       // expected translation language
);

if ($result['valid']) {
    echo "Translation is OK.\n";
    exit(0);
}

foreach ($result['defects'] as $i => $defect) {
    printf(
        "%d. [%s] %s\n",
        $i + 1,
        strtoupper(str_replace('_', ' ', $defect['type'])),
        $defect['message']
    );
}

[
    'valid'   => false,          // "usable": no quality ratio exceeded
    'error_count'   => 26,
    'warning_count' => 11,
    'defects' => [
        [
            'type'     => 'missing_caption',
            'severity' => 'warning',   // pairing-derived: advisory, never fails
            'message'  => 'Caption #1178 is missing in translation',
            'caption_number' => 1178,
            'original_text' => 'We need to go now.',
        ],
        [
            'type'     => 'merged_captions',
            'severity' => 'warning',
            'message'  => 'Original captions #316-317 are merged into translation caption #316',
            'source_start_caption' => 316,
            'source_end_caption'   => 317,
            'translation_caption' => 316,
            'caption_count' => 2,
        ],
        [
            'type'     => 'partial_translation',
            'severity' => 'warning', // advisory: never fails the verdict
            'message'  => 'Large block (captions 901-1400) detected as en instead of de - ...',
            'start_caption' => 901,
            'end_caption'   => 1400,
            'detected_language' => 'en',
            'confidence'    => 0.88,
        ],
        [
            'type'     => 'timestamp_mismatch',
            'severity' => 'error',
            'caption_number' => 301,
            'message'  => 'Caption #301 has timestamp drift',
            'original_start'     => 1334.627,
            'translation_start'  => 1336.627,
            'start_diff' => 2.0,
        ],
        [
            'type'     => 'invalid_format',
            'severity' => 'error',
            'line'     => 2112,
            'subtype'  => 'missing_timestamp',
            'message'  => 'Line 2112: expected a timing line (...) ...',
        ],
        [
            'type'     => 'unexpected_script',
            'severity' => 'error',
            'message'  => '42 of 18355 letters (0.23%) are written in a script that does not belong to language hu: cyrillic (42). Example characters: д л о',
            'foreign_chars' => 42,
            'total_letters' => 18355,
            'scripts'      => ['cyrillic' => 42],
            'examples'     => 'д л о',
            'language'     => 'hu',
            'ratio'        => 0.0023,
        ],
    ],
    'quality' => [
        'source_captions' => 1834,
        'aligned_pairs'   => 1808,
        'ratios' => [
            'content_loss'        => 0.0149,   // vs thresholds below (content captions only)
            'timestamp_drift'     => 0.0,
            'partial_translation' => 0.0,      // advisory, no threshold
            'merged'              => 0.006,    // advisory, no threshold
            'verbatim_copy'       => 0.0026,
            'near_verbatim_copy'  => 0.0065,
            'unexpected_script'   => 0.0,
            'reading_speed'       => 2.1,      // advisory peak factor, no threshold
            'pacing_error_share'  => 0.002,    // verdict gate: >= 2% (3-caption floor) fails
            'pacing_over_limit_share' => 0.04, // verdict gate: >= 50% fails
            'source_pollution_share' => 0.0,   // advisory; >= 1% demotes pacing
            'unaligned'           => 0.0142,   // advisory, no threshold
        ],
        'thresholds' => [
            'content_loss'        => 0.01,
            'timestamp_drift'     => 0.02,
            'partial_translation' => null,
            'merged'              => null,
            'verbatim_copy'       => 0.50,
            'near_verbatim_copy'  => 0.50,
            'unexpected_script'   => 0.0,
            'reading_speed'       => null,
            'pacing_error_share'  => 0.02,
            'pacing_over_limit_share' => 0.5,
            'source_pollution_share' => null,
            'unaligned'           => null,
        ],
        // Informational reading statistics - never affect the verdict
        'readability' => [
            'avg_cps'         => 12.4,
            'max_cps'         => 21.7,
            'max_cps_caption' => 183,
            'max_cpl'         => 42,
            'max_cpl_caption' => 402,
        ],
        'max_errors' => null,
        'strict'  => false,
        'reasons' => ['content loss 1.36% exceeds the threshold 1.00%'],
        'failed_gates' => ['content_loss'],   // machine-readable failing gates
    ],
]

$validator->validate($originalPath, $translationPath, 'de');

// Did the file fail BECAUSE of this defect type? (verdict-deciding only:
// an error-tier defect tolerated by the share gates does not count)
$validator->isFailing('untranslated_copy');   // true when the verbatim gates fired
$validator->isFailing('reading_speed');       // false on a merely slow-but-passing file

// Did this defect occur at all, regardless of the verdict?
$validator->hasDefect('missing_caption');                // any severity
$validator->hasDefect('partial_translation', 'warning'); // severity-filtered



rtValidator\SrtTranslationValidator;

$result = (new SrtTranslationValidator())->validate(
    'path/to/original.srt',
    'path/to/translation.srt',
    'de'
);

// Same encoding the CLI uses. JSON_UNESCAPED_UNICODE keeps umlauts,
// cyrillic etc. readable; drop JSON_PRETTY_PRINT for compact output.
$json = json_encode(
    $result,
    JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
);

file_put_contents('report.json', $json . "\n");

// Or serve it directly:
// header('Content-Type: application/json');
// echo $json;

$defects = array_values($result['defects']);
$byType  = [];
foreach ($defects as $defect) {
    $byType[$defect['type']] = ($byType[$defect['type']] ?? 0) + 1;
}
ksort($byType);

$envelope = [
    'valid'        => (bool)$result['valid'],
    'result'       => $result['valid'] ? 'passed' : 'failed',
    'original'     => 'path/to/original.srt',
    'translation'  => 'path/to/translation.srt',
    'language'     => 'de',
    'defect_count' => count($defects),
    'error_count'   => $result['error_count'],
    'warning_count' => $result['warning_count'],
    'defects_by_type' => $byType,
    'defects'      => $defects,
    'quality'      => $result['quality'],
];



rtValidator\SubtitleFormatValidator;

$validator = new SubtitleFormatValidator();

// Content already in memory:
$result = $validator->validateContent($srtContent, 'srt');

// Or a file on disk:
$result = $validator->validateFile('subtitles.srt');

printf(
    "format=%s valid=%s cues=%d\n",
    $result['format'],                  // 'srt' or 'vtt'
    $result['valid'] ? 'yes' : 'no',
    $result['cue_count']
);

foreach ($result['errors'] as $error) {
    printf("  line %d [%s] %s\n", $error['line'], $error['subtype'], $error['message']);
}



rtValidator\ReadabilityChecker;

// Option 1: bind a file at construction; analyze() then needs no arguments
$checker = new ReadabilityChecker('Movie.de.srt');
$analysis = $checker->analyze();

// Option 2: one-shot on a file
$analysis = (new ReadabilityChecker())->analyzeFile('Movie.de.srt');

// Option 3: content already in memory (downloaded, generated, ...)
$analysis = (new ReadabilityChecker())->analyzeContent($srtString);

$analysis = (new ReadabilityChecker())->analyzeFile('Movie.de.srt', 17.0, 37);

foreach ($analysis['problems'] as $problem) {
    printf(
        "#%d [%s] %s\n",
        $problem['caption'],   // 1-based caption number
        $problem['severity'],  // 'critical' or 'minor'
        $problem['text']
    );
    foreach ($problem['issues'] as $issue) {
        printf(
            "  - %s: %s exceeds limit %s (%s)\n",
            $issue['type'],    // reading_speed | line_length | line_count
            $issue['value'],
            $issue['limit'],
            $issue['severity']
        );
    }
}

if ($analysis['problems'] === []) {
    echo "Every caption is within the limits.\n";
}
bash
composer install          # dev dependencies (only needed for tests)
php -d phar.readonly=0 build/build-phar.php