Internet Roadtrip GPS TTS (Integrated with Minimap + Puter.js)

GPS TTS with street names and turn-by-turn directions integrated with the Minimap, using Puter.js for voice synthesis. Overlay sits above the minimap for real-time navigation feedback.

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey, Greasemonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्क्रिप्ट व्यवस्थापक एक्स्टेंशन इंस्टॉल करावे लागेल.

(माझ्याकडे आधीच युझर स्क्रिप्ट व्यवस्थापक आहे, मला इंस्टॉल करू द्या!)

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

(माझ्याकडे आधीच युझर स्टाईल व्यवस्थापक आहे, मला इंस्टॉल करू द्या!)

// ==UserScript==
// @name         Internet Roadtrip GPS TTS (Integrated with Minimap + Puter.js)
// @namespace    linktr.ee/gamerfronts
// @version      1.7
// @description  GPS TTS with street names and turn-by-turn directions integrated with the Minimap, using Puter.js for voice synthesis. Overlay sits above the minimap for real-time navigation feedback.
// @author       Gamerfronts
// @match        https://neal.fun/internet-roadtrip/*
// @license      MIT
// @grant        none
// ==/UserScript==

(() => {
    'use strict';

    /*** Load Puter.js library ***/
    const puterScript = document.createElement('script');
    puterScript.src = 'https://js.puter.com/v2/';
    document.head.appendChild(puterScript);

    /*** TTS Helper using Puter.js or fallback to speechSynthesis ***/
    const TTS = {
        enabled: true,
        usePuter: true,
        lang: 'en-US',
        rate: 1.0,
        volume: 1.0,
        async speak(text) {
            if (!this.enabled || !text) return;

            // Prefer Puter.js if available
            if (this.usePuter && window.puter?.ai?.txt2speech) {
                try {
                    const audio = await puter.ai.txt2speech(text, this.lang);
                    audio.volume = this.volume;
                    await audio.play();
                    return;
                } catch (err) {
                    console.warn('Puter.js TTS failed, using fallback:', err);
                    this._fallback(text);
                }
            } else {
                this._fallback(text);
            }
        },
        _fallback(text) {
            const u = new SpeechSynthesisUtterance(text);
            u.lang = this.lang;
            u.rate = this.rate;
            u.volume = this.volume;
            speechSynthesis.speak(u);
        }
    };

    /*** Reverse geocode with OpenStreetMap Nominatim ***/
    async function getStreetName(lat, lng) {
        try {
            const url = `https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}`;
            const resp = await fetch(url, {headers: {'User-Agent':'IR-TTS-Script'}});
            const data = await resp.json();
            if (data?.address) {
                const street = data.address.road || data.address.residential || data.address.pedestrian || '';
                const city = data.address.city || data.address.town || data.address.village || '';
                return {street, city};
            }
        } catch (e) {
            console.error('Reverse geocode error', e);
        }
        return {street:null, city:null};
    }

    /*** Patch main Vue stop change ***/
    const containerEl = document.querySelector('.container');
    let lastStop = '';

    function patchVue(vue) {
        if (!vue || vue.__ttsPatched) return;
        vue.__ttsPatched = true;

        const orig = vue.changeStop.bind(vue);
        vue.changeStop = async function() {
            const result = orig.apply(this, arguments);
            setTimeout(async () => {
                try {
                    const coords = this.currentCoords;
                    if (!coords) return;
                    const {street, city} = await getStreetName(coords.lat, coords.lng);
                    let text = '';
                    if (street) text = `Now arriving at ${street}${city ? ' in ' + city : ''}`;
                    else text = `Now arriving at destination`;

                    if (text !== lastStop) {
                        lastStop = text;
                        TTS.speak(text);
                        const st = document.getElementById('ir-tts-status');
                        if (st) st.textContent = text;
                    }
                } catch(e){console.error(e);}
            }, 50);
            return result;
        };
        console.log('IR GPS TTS: Vue patched.');
    }

    /*** Overlay UI above minimap ***/
    function createFloatingUI() {
        if (document.getElementById('ir-tts-ui')) return;
        const ui = document.createElement('div');
        ui.id = 'ir-tts-ui';

        Object.assign(ui.style, {
            position: 'fixed',
            bottom: '200px',  // placed above minimap
            left: '60px',
            zIndex: 9999999,
            background: 'rgba(0,0,0,0.75)',
            color: 'white',
            padding: '8px 10px',
            fontFamily: 'Arial, sans-serif',
            fontSize: '12px',
            borderRadius: '6px',
            minWidth: '180px',
            maxWidth: '230px',
            boxShadow: '0 0 6px rgba(0,0,0,0.4)',
            backdropFilter: 'blur(4px)',
            transition: 'opacity 0.3s ease'
        });

        const title = document.createElement('div');
        title.textContent = 'Roadtrip GPS TTS';
        title.style.fontWeight = '700';
        const status = document.createElement('div');
        status.id = 'ir-tts-status';
        status.textContent = 'Status: ready';

        ui.append(title, status);
        document.body.appendChild(ui);
    }

    /*** Optional Settings Tab Section ***/
    function injectTTSSettings() {
        const settingsContainer = document.querySelector('#settings-tab');
        if (!settingsContainer) return;
        const section = document.createElement('div');
        section.style.marginTop = '10px';
        section.style.padding = '5px';
        section.style.borderTop = '1px solid #ccc';

        const title = document.createElement('h3');
        title.textContent = 'GPS Voice Settings';
        title.style.fontSize = '14px';
        section.appendChild(title);

        // Enable checkbox
        const toggle = document.createElement('input');
        toggle.type = 'checkbox';
        toggle.checked = TTS.enabled;
        toggle.id = 'tts-enable';
        toggle.onchange = () => { TTS.enabled = toggle.checked; };
        const label = document.createElement('label');
        label.textContent = 'Enable TTS';
        label.htmlFor = 'tts-enable';
        label.style.marginLeft = '6px';
        section.append(toggle, label);

        // Use Puter toggle
        const puterToggle = document.createElement('input');
        puterToggle.type = 'checkbox';
        puterToggle.checked = TTS.usePuter;
        puterToggle.id = 'tts-puter';
        puterToggle.style.marginLeft = '12px';
        puterToggle.onchange = () => { TTS.usePuter = puterToggle.checked; };
        const puterLabel = document.createElement('label');
        puterLabel.textContent = 'Use Puter.js voice';
        puterLabel.htmlFor = 'tts-puter';
        puterLabel.style.marginLeft = '6px';
        section.append(document.createElement('br'), puterToggle, puterLabel);

        // Test button
        const testBtn = document.createElement('button');
        testBtn.textContent = '🔊 Test Voice';
        testBtn.style.marginTop = '6px';
        testBtn.onclick = () => TTS.speak('This is a test of the GPS voice navigation system.');
        section.append(document.createElement('br'), testBtn);

        settingsContainer.append(section);
    }

    /*** Wait for Vue instance & hook ***/
    const watcher = setInterval(async () => {
        const vue = containerEl?.__vue__;
        if (vue && typeof vue.changeStop === 'function') {
            clearInterval(watcher);
            injectTTSSettings();
            createFloatingUI();
            patchVue(vue);
        }
    }, 200);

    /*** Integrate with Minimap ***/
    async function waitForMinimap() {
        while (true) {
            try {
                if (window.IRF?.vdom?.map?.data?.marker) return window.IRF.vdom.map;
            } catch {}
            await new Promise(r => setTimeout(r, 500));
        }
    }

    (async () => {
        const vmap = await waitForMinimap();
        console.log('%c[IR GPS TTS] Connected to minimap marker.', 'color: lime;');

        let lastHeading = null;
        let lastStreet = null;
        let lastCoords = { lat: 0, lng: 0 };

        function getTurnDirection(current, previous) {
            if (previous === null || current === null) return null;
            const delta = ((current - previous + 540) % 360) - 180;
            if (Math.abs(delta) < 15) return null;
            if (delta > 15 && delta < 150) return "Turn right ahead";
            if (delta < -15 && delta > -150) return "Turn left ahead";
            return "Make a U-turn";
        }

        const marker = vmap.data.marker;
        const originalSetLngLat = marker.setLngLat;
        marker.setLngLat = new Proxy(originalSetLngLat, {
            async apply(target, thisArg, args) {
                try {
                    const [lng, lat] = args[0];
                    const vue = document.querySelector('.container').__vue__;
                    const heading = vue?.currentHeading || vue?.data?.currentHeading;

                    const turn = getTurnDirection(heading, lastHeading);
                    if (turn) TTS.speak(turn);
                    lastHeading = heading;

                    const moved = Math.hypot(lat - lastCoords.lat, lng - lastCoords.lng) > 0.001;
                    if (moved && Math.random() < 0.05) {
                        lastCoords = { lat, lng };
                        const {street, city} = await getStreetName(lat, lng);
                        if (street && street !== lastStreet) {
                            lastStreet = street;
                            TTS.speak(`Now driving on ${street}${city ? ' in ' + city : ''}.`);
                            const st = document.getElementById('ir-tts-status');
                            if (st) st.textContent = `Now on ${street}${city ? ', ' + city : ''}`;
                        }
                    }
                } catch (e) {
                    console.warn('TTS minimap hook error:', e);
                }
                return Reflect.apply(target, thisArg, args);
            }
        });

        console.log('%c[IR GPS TTS] Linked with minimap updates.', 'color: cyan;');
        TTS.speak('GPS voice navigation linked with minimap.');
    })();
})();