HEX
Server: LiteSpeed
System: Linux houston.panomity.com 6.8.0-100-generic #100-Ubuntu SMP PREEMPT_DYNAMIC Tue Jan 13 16:40:06 UTC 2026 x86_64
User: nudepix (1011)
PHP: 7.4.33
Disabled: pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,pcntl_unshare,
Upload Files
File: /home/kiwerkzeuge.de/public_html/service/speaches-service-manager.php
<?php
declare(strict_types=1);

function speaches_config(array $config): array
{
    $config['remote_endpoint'] = rtrim((string)($config['remote_endpoint'] ?? ''), '/');
    $config['service_unit'] = trim((string)($config['service_unit'] ?? 'speaches.service')) ?: 'speaches.service';
    $config['service_activity_file'] = trim((string)($config['service_activity_file'] ?? '/tmp/speaches-last-activity.txt')) ?: '/tmp/speaches-last-activity.txt';
    $config['service_start_timeout'] = max(10, min(900, (int)($config['service_start_timeout'] ?? 240)));
    $config['auto_stop_idle_seconds'] = max(30, min(7200, (int)($config['auto_stop_idle_seconds'] ?? 300)));
    $config['output_dir'] = trim((string)($config['output_dir'] ?? '')) ?: '/home/kiwerkzeuge.de/public_html/uploads/speaches';
    $config['output_url_base'] = '/' . trim((string)($config['output_url_base'] ?? '/uploads/speaches'), '/');
    $config['public_base_url'] = rtrim((string)($config['public_base_url'] ?? 'https://kiwerkzeuge.de'), '/');
    $config['default_model'] = trim((string)($config['default_model'] ?? '')) ?: 'speaches-ai/Kokoro-82M-v1.0-ONNX';
    $config['default_voice'] = trim((string)($config['default_voice'] ?? '')) ?: 'af_heart';
    $config['default_response_format'] = speaches_normalize_response_format((string)($config['default_response_format'] ?? 'wav'));
    $config['max_text_chars'] = max(50, min(20000, (int)($config['max_text_chars'] ?? 2000)));
    $config['execute_secret'] = (string)($config['execute_secret'] ?? '');
    return $config;
}

function speaches_service_touch_activity(array $config): string
{
    $token = time() . '-' . bin2hex(random_bytes(4));
    @file_put_contents((string)$config['service_activity_file'], $token, LOCK_EX);
    return $token;
}

function speaches_service_run_systemctl(array $config, string $action): bool
{
    if (!in_array($action, ['start', 'stop', 'restart'], true)) {
        return false;
    }
    $unitArg = escapeshellarg((string)$config['service_unit']);
    $cmds = [
        "sudo -n /bin/systemctl {$action} {$unitArg}",
        "sudo -n /usr/bin/systemctl {$action} {$unitArg}",
        "/bin/systemctl {$action} {$unitArg}",
        "/usr/bin/systemctl {$action} {$unitArg}",
    ];
    foreach ($cmds as $cmd) {
        @exec($cmd . ' >/dev/null 2>&1', $out, $code);
        if ($code === 0) {
            return true;
        }
    }
    return false;
}

function speaches_service_ping(array $config, int $timeoutSeconds = 4): array
{
    $url = rtrim((string)$config['remote_endpoint'], '/') . '/v1/models';
    if ($url === '/v1/models') {
        return ['ok' => false, 'status' => 0, 'error' => 'remote_endpoint fehlt'];
    }
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => max(1, $timeoutSeconds),
        CURLOPT_CONNECTTIMEOUT => min(3, max(1, $timeoutSeconds)),
        CURLOPT_HTTPHEADER => ['Accept: application/json'],
    ]);
    $response = curl_exec($ch);
    $error = (string)curl_error($ch);
    $status = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    if ($response === false) {
        return ['ok' => false, 'status' => $status, 'error' => ($error !== '' ? $error : 'nicht erreichbar')];
    }
    if ($status >= 200 && $status < 400) {
        $decoded = json_decode((string)$response, true);
        return ['ok' => true, 'status' => $status, 'payload' => is_array($decoded) ? $decoded : null];
    }
    return ['ok' => false, 'status' => $status, 'error' => 'HTTP ' . $status];
}

function speaches_service_ensure_ready(array $config): array
{
    $ping = speaches_service_ping($config, 3);
    if (($ping['ok'] ?? false) === true) {
        speaches_service_touch_activity($config);
        return ['ok' => true, 'started' => false, 'message' => 'Bereit'];
    }
    if (!($config['service_control_enabled'] ?? true)) {
        return ['ok' => false, 'started' => false, 'message' => 'Speaches ist offline.', 'detail' => (string)($ping['error'] ?? '')];
    }
    if (!speaches_service_run_systemctl($config, 'start')) {
        return ['ok' => false, 'started' => false, 'message' => 'Speaches konnte nicht gestartet werden.', 'detail' => 'systemctl start fehlgeschlagen'];
    }
    $deadline = microtime(true) + (int)$config['service_start_timeout'];
    $lastError = '';
    while (microtime(true) < $deadline) {
        usleep(500000);
        $probe = speaches_service_ping($config, 4);
        if (($probe['ok'] ?? false) === true) {
            speaches_service_touch_activity($config);
            return ['ok' => true, 'started' => true, 'message' => 'Speaches gestartet'];
        }
        $lastError = (string)($probe['error'] ?? '');
    }
    return ['ok' => false, 'started' => true, 'message' => 'Speaches startet noch oder ist nicht erreichbar.', 'detail' => $lastError];
}

function speaches_service_schedule_auto_stop(array $config): void
{
    if (!($config['service_control_enabled'] ?? true)) {
        return;
    }
    $token = speaches_service_touch_activity($config);
    $idleSeconds = (int)$config['auto_stop_idle_seconds'];
    $unitArg = escapeshellarg((string)$config['service_unit']);
    $activityFileArg = escapeshellarg((string)$config['service_activity_file']);
    $tokenArg = escapeshellarg($token);
    $cmd = '(sleep ' . $idleSeconds . '; CUR=$(cat ' . $activityFileArg . ' 2>/dev/null || true); if [ "$CUR" = ' . $tokenArg . ' ]; then '
         . 'sudo -n /bin/systemctl stop ' . $unitArg . ' >/dev/null 2>&1 || '
         . 'sudo -n /usr/bin/systemctl stop ' . $unitArg . ' >/dev/null 2>&1 || '
         . '/bin/systemctl stop ' . $unitArg . ' >/dev/null 2>&1 || '
         . '/usr/bin/systemctl stop ' . $unitArg . ' >/dev/null 2>&1; '
         . 'fi) >/dev/null 2>&1 &';
    @exec($cmd);
}

function speaches_is_local_request(): bool
{
    $remote = (string)($_SERVER['REMOTE_ADDR'] ?? '');
    return $remote === '127.0.0.1' || $remote === '::1';
}

function speaches_require_exec_auth(array $config, array $body): void
{
    if (speaches_is_local_request()) {
        return;
    }
    $secret = (string)($body['_gpuq_secret'] ?? '');
    $expected = (string)($config['execute_secret'] ?? '');
    if ($expected !== '' && hash_equals($expected, $secret)) {
        return;
    }
    http_response_code(403);
    echo json_encode(['ok' => false, 'message' => 'Forbidden']);
    exit;
}

function speaches_http_json(string $method, string $url, ?array $payload, int $timeoutSeconds = 60): array
{
    $ch = curl_init($url);
    $headers = ['Accept: application/json'];
    if ($payload !== null) {
        $body = json_encode($payload);
        if ($body === false) {
            return ['ok' => false, 'status' => 500, 'error' => 'JSON encode failed'];
        }
        $headers[] = 'Content-Type: application/json';
        curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
    }
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => max(1, $timeoutSeconds),
        CURLOPT_CONNECTTIMEOUT => min(5, max(1, $timeoutSeconds)),
        CURLOPT_CUSTOMREQUEST => strtoupper($method),
        CURLOPT_HTTPHEADER => $headers,
    ]);
    $response = curl_exec($ch);
    $status = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $error = (string)curl_error($ch);
    curl_close($ch);
    if ($response === false) {
        return ['ok' => false, 'status' => $status, 'error' => ($error !== '' ? $error : 'curl failed')];
    }
    $decoded = json_decode((string)$response, true);
    return [
        'ok' => $status >= 200 && $status < 400,
        'status' => $status,
        'json' => is_array($decoded) ? $decoded : null,
        'raw' => (string)$response,
        'error' => ($status >= 400 ? ('HTTP ' . $status) : ''),
    ];
}

function speaches_http_bytes(string $url, array $payload, int $timeoutSeconds = 300): array
{
    $body = json_encode($payload);
    if ($body === false) {
        return ['ok' => false, 'status' => 500, 'error' => 'JSON encode failed'];
    }
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => max(10, $timeoutSeconds),
        CURLOPT_CONNECTTIMEOUT => 5,
        CURLOPT_POST => true,
        CURLOPT_POSTFIELDS => $body,
        CURLOPT_HTTPHEADER => [
            'Accept: audio/*,application/octet-stream',
            'Content-Type: application/json',
        ],
    ]);
    $response = curl_exec($ch);
    $status = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $error = (string)curl_error($ch);
    curl_close($ch);
    if ($response === false) {
        return ['ok' => false, 'status' => $status, 'error' => ($error !== '' ? $error : 'curl failed')];
    }
    if ($status >= 400) {
        return ['ok' => false, 'status' => $status, 'error' => 'HTTP ' . $status, 'raw' => (string)$response];
    }
    return ['ok' => true, 'status' => $status, 'bytes' => (string)$response];
}

function speaches_normalize_response_format(string $format): string
{
    $value = strtolower(trim($format));
    if (!in_array($value, ['wav', 'mp3', 'flac', 'pcm'], true)) {
        return 'wav';
    }
    return $value;
}

function speaches_fetch_models_and_voices(array $config): array
{
    $ready = speaches_service_ensure_ready($config);
    if (!($ready['ok'] ?? false)) {
        return ['ok' => false, 'http_status' => 503, 'message' => (string)($ready['message'] ?? 'Speaches nicht bereit')];
    }
    register_shutdown_function('speaches_service_schedule_auto_stop', $config);
    $models = speaches_http_json('GET', rtrim((string)$config['remote_endpoint'], '/') . '/v1/models', null, 30);
    $voices = speaches_http_json('GET', rtrim((string)$config['remote_endpoint'], '/') . '/v1/audio/voices', null, 30);
    return [
        'ok' => ($models['ok'] ?? false) === true,
        'http_status' => (int)($models['status'] ?? 200),
        'models' => $models['json']['data'] ?? $models['json']['models'] ?? [],
        'voices' => $voices['json']['voices'] ?? [],
        'message' => ($models['ok'] ?? false) ? 'loaded' : ((string)($models['error'] ?? 'models request failed')),
    ];
}

function speaches_ensure_model_installed(array $config, string $modelId): array
{
    $base = rtrim((string)$config['remote_endpoint'], '/');
    $current = speaches_http_json('GET', $base . '/v1/models/' . rawurlencode($modelId), null, 30);
    if (($current['ok'] ?? false) === true) {
        return ['ok' => true, 'downloaded' => false];
    }
    $download = speaches_http_json('POST', $base . '/v1/models/' . rawurlencode($modelId), [], 300);
    if (($download['ok'] ?? false) === true) {
        return ['ok' => true, 'downloaded' => true];
    }
    return ['ok' => false, 'message' => (string)($download['error'] ?? 'model download failed')];
}

function speaches_build_output_target(array $config, string $basename, string $format): array
{
    $safeBase = preg_replace('/[^a-zA-Z0-9_-]+/', '-', $basename) ?: ('speaches-' . date('His'));
    $dateDir = date('Y-m-d');
    $root = rtrim((string)$config['output_dir'], '/');
    $dir = $root . '/' . $dateDir;
    if (!is_dir($dir) && !@mkdir($dir, 0775, true) && !is_dir($dir)) {
        return ['ok' => false, 'message' => 'output dir could not be created'];
    }
    $filename = $safeBase . '-' . date('His') . '.' . $format;
    $path = $dir . '/' . $filename;
    $url = rtrim((string)$config['public_base_url'], '/') . rtrim((string)$config['output_url_base'], '/') . '/' . $dateDir . '/' . $filename;
    return ['ok' => true, 'path' => $path, 'url' => $url];
}

function speaches_render_to_file(array $config, array $request): array
{
    $config = speaches_config($config);
    $ready = speaches_service_ensure_ready($config);
    if (!($ready['ok'] ?? false)) {
        return ['ok' => false, 'http_status' => 503, 'message' => (string)($ready['message'] ?? 'Speaches nicht bereit')];
    }
    register_shutdown_function('speaches_service_schedule_auto_stop', $config);

    $input = trim((string)($request['input'] ?? ''));
    if ($input === '') {
        return ['ok' => false, 'http_status' => 400, 'message' => 'input is required'];
    }
    $inputLength = function_exists('mb_strlen') ? mb_strlen($input) : strlen($input);
    if ($inputLength > (int)$config['max_text_chars']) {
        return ['ok' => false, 'http_status' => 400, 'message' => 'input exceeds max_text_chars'];
    }

    $model = trim((string)($request['model'] ?? $config['default_model'])) ?: (string)$config['default_model'];
    $voice = trim((string)($request['voice'] ?? $config['default_voice'])) ?: (string)$config['default_voice'];
    $speed = (float)($request['speed'] ?? 1.0);
    $speed = max(0.5, min(2.0, $speed));
    $format = speaches_normalize_response_format((string)($request['response_format'] ?? $config['default_response_format']));
    $basename = trim((string)($request['output_basename'] ?? ('speaches-' . date('His'))));

    $modelReady = speaches_ensure_model_installed($config, $model);
    if (!($modelReady['ok'] ?? false)) {
        return ['ok' => false, 'http_status' => 502, 'message' => (string)($modelReady['message'] ?? 'model install failed')];
    }

    $audio = speaches_http_bytes(
        rtrim((string)$config['remote_endpoint'], '/') . '/v1/audio/speech',
        [
            'input' => $input,
            'model' => $model,
            'voice' => $voice,
            'speed' => $speed,
            'response_format' => $format,
        ],
        300
    );
    if (!($audio['ok'] ?? false)) {
        return ['ok' => false, 'http_status' => (int)($audio['status'] ?? 502), 'message' => (string)($audio['error'] ?? 'speech request failed')];
    }

    $target = speaches_build_output_target($config, $basename, $format);
    if (!($target['ok'] ?? false)) {
        return ['ok' => false, 'http_status' => 500, 'message' => (string)($target['message'] ?? 'output target failed')];
    }
    if (@file_put_contents((string)$target['path'], (string)$audio['bytes']) === false) {
        return ['ok' => false, 'http_status' => 500, 'message' => 'audio file could not be written'];
    }

    return [
        'ok' => true,
        'http_status' => 200,
        'audio_path' => (string)$target['path'],
        'audio_url' => (string)$target['url'],
        'model' => $model,
        'voice' => $voice,
        'response_format' => $format,
        'bytes' => strlen((string)$audio['bytes']),
    ];
}