├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── README.md ├── README_ENG.md ├── backend ├── Dockerfile ├── __init__.py └── flask │ ├── Pipfile │ ├── Pipfile.lock │ ├── __init__.py │ ├── app │ ├── __init__.py │ ├── config.py │ ├── models.py │ ├── oauth.py │ └── socket.py │ ├── debug.py │ ├── manage.py │ ├── migrations │ ├── README │ ├── alembic.ini │ ├── env.py │ ├── script.py.mako │ └── versions │ │ ├── 59f3082483dd_.py │ │ ├── 763fc9f85cf2_.py │ │ ├── 875e320523d6_.py │ │ └── ca9eec44e3f1_.py │ └── run.py ├── docker-compose.example.yml ├── frontend ├── overlay-sans.html ├── overlay.html ├── profile.png ├── recognition.html ├── sans.png └── voice_sans.mp3 ├── sample.gif └── settings.env.example /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: ['https://www.buymeacoffee.com/nesswit'] 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vscode/ 3 | settings.env 4 | settings.dev.env 5 | docker-compose.yml 6 | docker-compose.dev.yml 7 | 8 | __pycache__ 9 | *.py[co] 10 | 11 | backend/log 12 | backend/flask/.venv 13 | 14 | db-data/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Nesswit 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [English](README_ENG.md) 2 | 3 | # Open Captions Overlay 4 | 5 | ![Sample](./sample.gif) 6 | 7 | `Open Captions Overlay`는 자막을 방송 송출 프로그램(`OBS`, `XSplit` 등)에 표시하기 위한 서비스입니다. 8 | 9 | 이 서비스을 사용하면 마이크를 통해 말한 내용을 방송 화면에 실시간으로 자막처럼 표시할 수 있습니다. 10 | 11 | 이 프로젝트는 [Closed Captions for Streams](https://www.twitch.tv/ext/xxwoffr2lnpxrgpq228mawvdgxetip)에서 영감을 받아 제작되었습니다. 12 | 13 | [데모 동영상](https://youtu.be/CAIVO6aMgs4) 14 | 15 | ## 사용방법 16 | 17 | ### 1. 음성 인식 설정 18 | 19 | 1. [음성 인식 사이트](https://oc-overlay.update.sh/recognition)에 __"최신 버전 크롬"__ 으로 접속합니다. 20 | 2. `음성 인식 사이트`에 방송하고자 하는 트위치 계정으로 로그인합니다. 21 | 3. `음성 인식 사이트`의 `인식 시작` 버튼을 클릭하고 마이크 사용 권한을 허용합니다. 22 | 4. `음성 인식 사이트`에서 마이크에 말한 내용을 인식한 내용이 출력되는 것을 확인합니다. 23 | 24 | 마이크에 말한 내용이 정상적으로 인식되어 사이트에 출력된다면, 음성 인식 설정을 모두 완료한 것입니다. 25 | 26 | ### 2. 방송 송출 프로그램 설정 27 | 28 | #### 3.1. OBS Studio 29 | 30 | 1. [음성 인식 사이트](https://oc-overlay.update.sh/recognition)에서 표시되는 `오버레이 주소`를 OBS에 "브라우저" 소스의 URL로 등록합니다. 31 | 2. 캔버스 크기 1280x720에서 권장하는 브라우저 소스 너비와 높이는 840, 210입니다. 32 | 33 | #### 3.1. XSplit Broadcaster 34 | 35 | 1. [음성 인식 사이트](https://oc-overlay.update.sh/recognition)에서 표시되는 `오버레이 주소`를 웹 페이지 소스 추가 창에서 URL로 등록합니다. 36 | 2. 캔버스 크기 1280x720에서 권장하는 브라우저 소스 너비와 높이는 840, 210입니다. 37 | 38 | ### 4. 설정 마무리 39 | 40 | 해당 설정을 완료한 후, 실제 방송에서 자막을 사용하시고자 할 때에는 [음성 인식 사이트](https://oc-overlay.update.sh/recognition)에 접속하셔서 `인식 시작` 버튼을 클릭하시면 됩니다. 41 | 42 | 만약 사용을 종료하시려면 음성 인식 사이트의 `인식 종료` 버튼을 클릭하거나 창을 종료합니다. 43 | 44 | ## 커스터마이징 45 | 46 | ### 1. 오버레이에 표시되는 프로필 이미지 변경 47 | 48 | 방송 송출 프로그램에서 Open Captions Overlay 브라우저 소스의 CSS에 다음 내용을 추가합니다. 49 | 50 | ```css 51 | :root { 52 | --profile-url: url(https://i.postimg.cc/Qtsn3rc7/profile.png); 53 | } 54 | ``` 55 | 56 | 여기서 `https://i.postimg.cc/Qtsn3rc7/profile.png` 값을 변경할 프로필 이미지의 URL로 변경해주세요. 57 | 58 | (이미지 URL은 [postimages.org](https://postimages.org/)와 같은 이미지 호스팅 서비스를 통해 생성할 수 있습니다.) 59 | 60 | ### 2. 오버레이에 표시되는 강조 색상 변경 61 | 62 | 방송 송출 프로그램에서 Open Captions Overlay 브라우저 소스의 CSS에 다음 내용을 추가합니다. 63 | 64 | ```css 65 | :root { 66 | --accent-color: #95BBDF; 67 | } 68 | ``` 69 | 70 | 여기서 `#95BBDF` 값을 변경할 강조 색상 코드로 변경해주세요. 71 | 72 | ### 3. 오버레이의 바탕이 투명하지 않은 경우 73 | 74 | 방송 송출 프로그램에서 Open Captions Overlay 브라우저 소스의 CSS에 다음 내용을 추가합니다. 75 | 76 | ```css 77 | body { 78 | background-color: rgba(0, 0, 0, 0); 79 | margin: 0px auto; 80 | overflow: hidden; 81 | } 82 | ``` 83 | 84 | ## Development 85 | 86 | 이하의 내용은 일반 사용자가 아닌 개발자를 위한 내용입니다. 87 | 88 | ### Init project 89 | 90 | ``` 91 | docker-compose run --rm backend python manage.py db upgrade 92 | ``` 93 | 94 | ### Reinstall dependencies 95 | 96 | ``` 97 | docker-compose run --rm backend pipenv lock --pre 98 | docker-compose build --force-rm 99 | ``` 100 | 101 | ## License 102 | 103 | 본 프로젝트는 [MIT License](./LICENSE) 하에 제공됩니다. 104 | -------------------------------------------------------------------------------- /README_ENG.md: -------------------------------------------------------------------------------- 1 | [한국어](https://github.com/yf-dev/OpenCaptionsOverlay) 2 | 3 | # Open Captions Overlay 4 | 5 | ![Sample](./sample.gif) 6 | 7 | `Open Captions Overlay` is a service to display subtitles on broadcast programs such as `OBS` and `XSplit`. 8 | 9 | If you use this service, the voice spoken through the microphone can be displayed as subtitles in real time on the broadcast screen. 10 | 11 | This project was inspired by [Closed Captions for Streams](https://www.twitch.tv/ext/xxwoffr2lnpxrgpq228mawvdgxetip). 12 | 13 | [Demo Video](https://youtu.be/CAIVO6aMgs4) 14 | 15 | ## Usage 16 | 17 | ### 1. Setting the speech recognition 18 | 19 | 1. Access [Voice Recognition Site](https://cc-overlay.update.sh/recognition) using __"latest version of Chrome"__. 20 | 2. Log in to the `Voice Recognition Site` with the Twitch account you want to broadcast. 21 | 3. Click the `Start Recognition` button and allow the microphone permission. 22 | 4. Make sure that what you say on the microphone is printed out. 23 | 24 | If what you said on your microphone is successfully printed, you have completed all of the voice recognition settings. 25 | 26 | ### 2. Setting the broadcast transmission program 27 | 28 | #### 3.1. OBS Studio 29 | 30 | 1. Register the 'overlay address' displayed in [Voice Recognition Site](https://cc-overlay.update.sh/recognition) as the browser source URL of OBS Studio. 31 | 2. When your canvas size is `1280x720`, the recommended browser size is `840x210`. 32 | 33 | #### 3.1. XSplit Broadcaster 34 | 35 | 1. Register the 'overlay address' displayed in [Voice Recognition Site](https://cc-overlay.update.sh/recognition) in the URL field of the Add Web Page Source window. 36 | 2. When your canvas size is `1280x720`, the recommended browser size is `840x210`. 37 | 38 | ### 4. Finish settings 39 | 40 | After completing the settings, if you want to use subtitles in the actual broadcast, access the [Voice Recognition Site](https://cc-overlay.update.sh/recognition) and click the `Start Recognition` button. 41 | 42 | If you want to end the use, click the `End Recognition` button on the `Voice Recognition Site` or close the window. 43 | 44 | ## Customizing 45 | 46 | ### 1. Change your profile image displayed on the overlay 47 | 48 | Add the following content to the CSS of the browser source `Open Captions Overlay` of the broadcast program. 49 | 50 | ```css 51 | :root { 52 | --profile-url: url(https://i.postimg.cc/Qtsn3rc7/profile.png); 53 | } 54 | ``` 55 | 56 | Change (`https://i.postimg.cc/Qtsn3rc7/profile.png`) to the profile image URL you want to change. 57 | 58 | (Image URLs can be created through image hosting services such as [postimages.org](https://postimages.org/).) 59 | 60 | ### 2. Change the highlight color displayed on the overlay 61 | 62 | Add the following content to the CSS of the browser source `Open Captions Overlay` of the broadcast program. 63 | 64 | ```css 65 | :root { 66 | --accent-color: #95BBDF; 67 | } 68 | ``` 69 | 70 | Change the `#95BBDF` value to the highlight color code you want to change. 71 | 72 | ### 3. When the overlay background is not transparent 73 | 74 | Add the following content to the CSS of the browser source `Open Captions Overlay` of the broadcast program. 75 | 76 | ```css 77 | body { 78 | background-color: rgba(0, 0, 0, 0); 79 | margin: 0px auto; 80 | overflow: hidden; 81 | } 82 | ``` 83 | 84 | ## Development 85 | 86 | Below for developers 87 | 88 | ### Init project 89 | 90 | ``` 91 | docker-compose run --rm backend python manage.py db upgrade 92 | ``` 93 | 94 | ### Reinstall dependencies 95 | 96 | ``` 97 | docker-compose run --rm backend pipenv lock --pre 98 | docker-compose build --force-rm 99 | ``` 100 | 101 | ## License 102 | 103 | This project is available under [MIT License](./LICENSE). 104 | -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7.4-buster 2 | 3 | RUN apt-get update && apt-get -y install \ 4 | libpq-dev \ 5 | pipenv && \ 6 | apt-get clean && rm -rf /var/cache/apt/* && rm -rf /var/lib/apt/lists/* 7 | 8 | RUN pip install virtualenv 9 | 10 | COPY flask/Pipfile /var/www/flask/Pipfile 11 | COPY flask/Pipfile.lock /var/www/flask/Pipfile.lock 12 | WORKDIR /var/www/flask 13 | RUN pipenv lock --requirements > requirements.txt 14 | RUN pip install -r requirements.txt 15 | 16 | EXPOSE 80 17 | EXPOSE 443 18 | 19 | CMD ["gunicorn", "--worker-class", "eventlet", "-w", "1", "-b", "0.0.0.0:80", "--forwarded-allow-ips", "*", "app:app"] 20 | -------------------------------------------------------------------------------- /backend/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yf-dev/OpenCaptionsOverlay/433f5a83e074f8f808b92bf323ce4eee70337496/backend/__init__.py -------------------------------------------------------------------------------- /backend/flask/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.python.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | Flask = "==1.1.1" 8 | Flask-Migrate = "==2.5.2" 9 | Flask-SQLAlchemy = "==2.4.0" 10 | Flask-Script = "==2.0.6" 11 | Flask-SocketIO = "==4.2.1" 12 | Flask-Login = "==0.4.1" 13 | psycopg2 = "==2.8.3" 14 | eventlet = "==0.25.1" 15 | gunicorn = "==19.9.0" 16 | blinker = "==1.4" 17 | 18 | [requires] 19 | python_version = "3.7" 20 | 21 | [pipenv] 22 | allow_prereleases = true 23 | 24 | [packages.flask-dance] 25 | extras = [ "sqla",] 26 | version = "==2.2.0" 27 | -------------------------------------------------------------------------------- /backend/flask/Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "2e313d4370e01ef8c6e51ed0a6d06fefa0435d93996b653d02dbe2d0fda0fbff" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.7" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.python.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "alembic": { 20 | "hashes": [ 21 | "sha256:5609afbb2ab142a991b15ae436347c475f8a517f1610f2fd1b09cdca7c311f3f" 22 | ], 23 | "version": "==1.2.0" 24 | }, 25 | "blinker": { 26 | "hashes": [ 27 | "sha256:471aee25f3992bd325afa3772f1063dbdbbca947a041b8b89466dc00d606f8b6" 28 | ], 29 | "index": "pypi", 30 | "version": "==1.4" 31 | }, 32 | "certifi": { 33 | "hashes": [ 34 | "sha256:e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50", 35 | "sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef" 36 | ], 37 | "version": "==2019.9.11" 38 | }, 39 | "chardet": { 40 | "hashes": [ 41 | "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", 42 | "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" 43 | ], 44 | "version": "==3.0.4" 45 | }, 46 | "click": { 47 | "hashes": [ 48 | "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", 49 | "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" 50 | ], 51 | "version": "==7.0" 52 | }, 53 | "dnspython": { 54 | "hashes": [ 55 | "sha256:36c5e8e38d4369a08b6780b7f27d790a292b2b08eea01607865bf0936c558e01", 56 | "sha256:f69c21288a962f4da86e56c4905b49d11aba7938d3d740e80d9e366ee4f1632d" 57 | ], 58 | "version": "==1.16.0" 59 | }, 60 | "eventlet": { 61 | "hashes": [ 62 | "sha256:658b1cd80937adc1a4860de2841e0528f64e2ca672885c4e00fc0e2217bde6b1", 63 | "sha256:6c9c625af48424c4680d89314dbe45a76cc990cf002489f9469ff214b044ffc1" 64 | ], 65 | "index": "pypi", 66 | "version": "==0.25.1" 67 | }, 68 | "flask": { 69 | "hashes": [ 70 | "sha256:13f9f196f330c7c2c5d7a5cf91af894110ca0215ac051b5844701f2bfd934d52", 71 | "sha256:45eb5a6fd193d6cf7e0cf5d8a5b31f83d5faae0293695626f539a823e93b13f6" 72 | ], 73 | "index": "pypi", 74 | "version": "==1.1.1" 75 | }, 76 | "flask-dance": { 77 | "hashes": [ 78 | "sha256:83d6f8d684150ac8fe7d4f2ad8d71170c3233831a09601eb0e5b40d0c28e337d", 79 | "sha256:ad58ce046454916df3692ba38ae6ee0e0344ee227999124b7b86d923f4a68262", 80 | "sha256:c3fd1da1c93ada28092e83a5eb843def82701d6d56bd03a512a2e008a25ec106" 81 | ], 82 | "index": "pypi", 83 | "version": "==2.2.0" 84 | }, 85 | "flask-login": { 86 | "hashes": [ 87 | "sha256:c815c1ac7b3e35e2081685e389a665f2c74d7e077cb93cecabaea352da4752ec" 88 | ], 89 | "index": "pypi", 90 | "version": "==0.4.1" 91 | }, 92 | "flask-migrate": { 93 | "hashes": [ 94 | "sha256:6fb038be63d4c60727d5dfa5f581a6189af5b4e2925bc378697b4f0a40cfb4e1", 95 | "sha256:a96ff1875a49a40bd3e8ac04fce73fdb0870b9211e6168608cbafa4eb839d502" 96 | ], 97 | "index": "pypi", 98 | "version": "==2.5.2" 99 | }, 100 | "flask-script": { 101 | "hashes": [ 102 | "sha256:6425963d91054cfcc185807141c7314a9c5ad46325911bd24dcb489bd0161c65" 103 | ], 104 | "index": "pypi", 105 | "version": "==2.0.6" 106 | }, 107 | "flask-socketio": { 108 | "hashes": [ 109 | "sha256:2172dff1e42415ba480cee02c30c2fc833671ff326f1598ee3d69aa02cf768ec", 110 | "sha256:7ff5b2f5edde23e875a8b0abf868584e5706e11741557449bc5147df2cd78268" 111 | ], 112 | "index": "pypi", 113 | "version": "==4.2.1" 114 | }, 115 | "flask-sqlalchemy": { 116 | "hashes": [ 117 | "sha256:0c9609b0d72871c540a7945ea559c8fdf5455192d2db67219509aed680a3d45a", 118 | "sha256:8631bbea987bc3eb0f72b1f691d47bd37ceb795e73b59ab48586d76d75a7c605" 119 | ], 120 | "index": "pypi", 121 | "version": "==2.4.0" 122 | }, 123 | "greenlet": { 124 | "hashes": [ 125 | "sha256:000546ad01e6389e98626c1367be58efa613fa82a1be98b0c6fc24b563acc6d0", 126 | "sha256:0d48200bc50cbf498716712129eef819b1729339e34c3ae71656964dac907c28", 127 | "sha256:23d12eacffa9d0f290c0fe0c4e81ba6d5f3a5b7ac3c30a5eaf0126bf4deda5c8", 128 | "sha256:37c9ba82bd82eb6a23c2e5acc03055c0e45697253b2393c9a50cef76a3985304", 129 | "sha256:51503524dd6f152ab4ad1fbd168fc6c30b5795e8c70be4410a64940b3abb55c0", 130 | "sha256:8041e2de00e745c0e05a502d6e6db310db7faa7c979b3a5877123548a4c0b214", 131 | "sha256:81fcd96a275209ef117e9ec91f75c731fa18dcfd9ffaa1c0adbdaa3616a86043", 132 | "sha256:853da4f9563d982e4121fed8c92eea1a4594a2299037b3034c3c898cb8e933d6", 133 | "sha256:8b4572c334593d449113f9dc8d19b93b7b271bdbe90ba7509eb178923327b625", 134 | "sha256:9416443e219356e3c31f1f918a91badf2e37acf297e2fa13d24d1cc2380f8fbc", 135 | "sha256:9854f612e1b59ec66804931df5add3b2d5ef0067748ea29dc60f0efdcda9a638", 136 | "sha256:99a26afdb82ea83a265137a398f570402aa1f2b5dfb4ac3300c026931817b163", 137 | "sha256:a19bf883b3384957e4a4a13e6bd1ae3d85ae87f4beb5957e35b0be287f12f4e4", 138 | "sha256:a9f145660588187ff835c55a7d2ddf6abfc570c2651c276d3d4be8a2766db490", 139 | "sha256:ac57fcdcfb0b73bb3203b58a14501abb7e5ff9ea5e2edfa06bb03035f0cff248", 140 | "sha256:bcb530089ff24f6458a81ac3fa699e8c00194208a724b644ecc68422e1111939", 141 | "sha256:beeabe25c3b704f7d56b573f7d2ff88fc99f0138e43480cecdfcaa3b87fe4f87", 142 | "sha256:d634a7ea1fc3380ff96f9e44d8d22f38418c1c381d5fac680b272d7d90883720", 143 | "sha256:d97b0661e1aead761f0ded3b769044bb00ed5d33e1ec865e891a8b128bf7c656" 144 | ], 145 | "version": "==0.4.15" 146 | }, 147 | "gunicorn": { 148 | "hashes": [ 149 | "sha256:aa8e0b40b4157b36a5df5e599f45c9c76d6af43845ba3b3b0efe2c70473c2471", 150 | "sha256:fa2662097c66f920f53f70621c6c58ca4a3c4d3434205e608e121b5b3b71f4f3" 151 | ], 152 | "index": "pypi", 153 | "version": "==19.9.0" 154 | }, 155 | "idna": { 156 | "hashes": [ 157 | "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", 158 | "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" 159 | ], 160 | "version": "==2.8" 161 | }, 162 | "itsdangerous": { 163 | "hashes": [ 164 | "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19", 165 | "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749" 166 | ], 167 | "version": "==1.1.0" 168 | }, 169 | "jinja2": { 170 | "hashes": [ 171 | "sha256:065c4f02ebe7f7cf559e49ee5a95fb800a9e4528727aec6f24402a5374c65013", 172 | "sha256:14dd6caf1527abb21f08f86c784eac40853ba93edb79552aa1e4b8aef1b61c7b" 173 | ], 174 | "version": "==2.10.1" 175 | }, 176 | "mako": { 177 | "hashes": [ 178 | "sha256:a36919599a9b7dc5d86a7a8988f23a9a3a3d083070023bab23d64f7f1d1e0a4b" 179 | ], 180 | "version": "==1.1.0" 181 | }, 182 | "markupsafe": { 183 | "hashes": [ 184 | "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", 185 | "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", 186 | "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", 187 | "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", 188 | "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", 189 | "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", 190 | "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", 191 | "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", 192 | "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", 193 | "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", 194 | "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", 195 | "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", 196 | "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", 197 | "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", 198 | "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", 199 | "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", 200 | "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", 201 | "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", 202 | "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", 203 | "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", 204 | "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", 205 | "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", 206 | "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", 207 | "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", 208 | "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", 209 | "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", 210 | "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", 211 | "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7" 212 | ], 213 | "version": "==1.1.1" 214 | }, 215 | "monotonic": { 216 | "hashes": [ 217 | "sha256:23953d55076df038541e648a53676fb24980f7a1be290cdda21300b3bc21dfb0", 218 | "sha256:552a91f381532e33cbd07c6a2655a21908088962bb8fa7239ecbcc6ad1140cc7" 219 | ], 220 | "version": "==1.5" 221 | }, 222 | "oauthlib": { 223 | "hashes": [ 224 | "sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889", 225 | "sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea" 226 | ], 227 | "version": "==3.1.0" 228 | }, 229 | "psycopg2": { 230 | "hashes": [ 231 | "sha256:128d0fa910ada0157bba1cb74a9c5f92bb8a1dca77cf91a31eb274d1f889e001", 232 | "sha256:227fd46cf9b7255f07687e5bde454d7d67ae39ca77e170097cdef8ebfc30c323", 233 | "sha256:2315e7f104681d498ccf6fd70b0dba5bce65d60ac92171492bfe228e21dcc242", 234 | "sha256:4b5417dcd2999db0f5a891d54717cfaee33acc64f4772c4bc574d4ff95ed9d80", 235 | "sha256:640113ddc943522aaf71294e3f2d24013b0edd659b7820621492c9ebd3a2fb0b", 236 | "sha256:897a6e838319b4bf648a574afb6cabcb17d0488f8c7195100d48d872419f4457", 237 | "sha256:8dceca81409898c870e011c71179454962dec152a1a6b86a347f4be74b16d864", 238 | "sha256:b1b8e41da09a0c3ef0b3d4bb72da0dde2abebe583c1e8462973233fd5ad0235f", 239 | "sha256:cb407fccc12fc29dc331f2b934913405fa49b9b75af4f3a72d0f50f57ad2ca23", 240 | "sha256:d3a27550a8185e53b244ad7e79e307594b92fede8617d80200a8cce1fba2c60f", 241 | "sha256:f0e6b697a975d9d3ccd04135316c947dd82d841067c7800ccf622a8717e98df1" 242 | ], 243 | "index": "pypi", 244 | "version": "==2.8.3" 245 | }, 246 | "python-dateutil": { 247 | "hashes": [ 248 | "sha256:7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb", 249 | "sha256:c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e" 250 | ], 251 | "version": "==2.8.0" 252 | }, 253 | "python-editor": { 254 | "hashes": [ 255 | "sha256:1bf6e860a8ad52a14c3ee1252d5dc25b2030618ed80c022598f00176adc8367d", 256 | "sha256:51fda6bcc5ddbbb7063b2af7509e43bd84bfc32a4ff71349ec7847713882327b", 257 | "sha256:5f98b069316ea1c2ed3f67e7f5df6c0d8f10b689964a4a811ff64f0106819ec8", 258 | "sha256:c3da2053dbab6b29c94e43c486ff67206eafbe7eb52dbec7390b5e2fb05aac77", 259 | "sha256:ea87e17f6ec459e780e4221f295411462e0d0810858e055fc514684350a2f522" 260 | ], 261 | "version": "==1.0.4" 262 | }, 263 | "python-engineio": { 264 | "hashes": [ 265 | "sha256:2a4c874aea686e79f8ea9efc30748110df581df6d577d18bb5eaa1a8f2199d12", 266 | "sha256:eae09ef72bbb13e66ad0bbe2fbb87271e0ba5e2ec8c6693c6be1e14239564e32" 267 | ], 268 | "version": "==3.9.3" 269 | }, 270 | "python-socketio": { 271 | "hashes": [ 272 | "sha256:506b2cf7a520b40ea0b3f25e1272eff8de134dce6f471c1f6bc0de8c90fe8c57", 273 | "sha256:d4e2c23241afa0aae2a5bcc107523b2fcc71f5020df89a093f3634eb48955967" 274 | ], 275 | "version": "==4.3.1" 276 | }, 277 | "requests": { 278 | "hashes": [ 279 | "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", 280 | "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31" 281 | ], 282 | "version": "==2.22.0" 283 | }, 284 | "requests-oauthlib": { 285 | "hashes": [ 286 | "sha256:bd6533330e8748e94bf0b214775fed487d309b8b8fe823dc45641ebcd9a32f57", 287 | "sha256:d3ed0c8f2e3bbc6b344fa63d6f933745ab394469da38db16bdddb461c7e25140", 288 | "sha256:dd5a0499abfefd087c6dd96693cbd5bfd28aa009719a7f85ab3fabe3956ef19a" 289 | ], 290 | "version": "==1.2.0" 291 | }, 292 | "six": { 293 | "hashes": [ 294 | "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", 295 | "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" 296 | ], 297 | "version": "==1.12.0" 298 | }, 299 | "sqlalchemy": { 300 | "hashes": [ 301 | "sha256:2f8ff566a4d3a92246d367f2e9cd6ed3edeef670dcd6dda6dfdc9efed88bcd80" 302 | ], 303 | "version": "==1.3.8" 304 | }, 305 | "sqlalchemy-utils": { 306 | "hashes": [ 307 | "sha256:6689b29d7951c5c7c4d79fa6b8c95f9ff9ec708b07aa53f82060599bd14dcc88" 308 | ], 309 | "version": "==0.34.2" 310 | }, 311 | "urllib3": { 312 | "hashes": [ 313 | "sha256:2f3eadfea5d92bc7899e75b5968410b749a054b492d5a6379c1344a1481bc2cb", 314 | "sha256:9c6c593cb28f52075016307fc26b0a0f8e82bc7d1ff19aaaa959b91710a56c47" 315 | ], 316 | "version": "==1.25.5" 317 | }, 318 | "urlobject": { 319 | "hashes": [ 320 | "sha256:47b2e20e6ab9c8366b2f4a3566b6ff4053025dad311c4bb71279bbcfa2430caa" 321 | ], 322 | "version": "==2.4.3" 323 | }, 324 | "werkzeug": { 325 | "hashes": [ 326 | "sha256:7280924747b5733b246fe23972186c6b348f9ae29724135a6dfc1e53cea433e7", 327 | "sha256:e5f4a1f98b52b18a93da705a7458e55afb26f32bff83ff5d19189f92462d65c4" 328 | ], 329 | "version": "==0.16.0" 330 | } 331 | }, 332 | "develop": {} 333 | } 334 | -------------------------------------------------------------------------------- /backend/flask/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yf-dev/OpenCaptionsOverlay/433f5a83e074f8f808b92bf323ce4eee70337496/backend/flask/__init__.py -------------------------------------------------------------------------------- /backend/flask/app/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template, redirect, url_for, request, session 2 | from flask_migrate import Migrate 3 | from flask_sqlalchemy import SQLAlchemy 4 | from flask_socketio import SocketIO 5 | from flask_login import LoginManager, current_user 6 | 7 | from .config import Config 8 | 9 | 10 | app = Flask( 11 | __name__, 12 | static_folder="/frontend", 13 | template_folder="/frontend", 14 | ) 15 | app.config.from_object(Config) 16 | 17 | app.debug = True 18 | 19 | db = SQLAlchemy(app) 20 | migrate = Migrate(app, db) 21 | 22 | login_manager = LoginManager() 23 | login_manager.init_app(app) 24 | 25 | socketio = SocketIO( 26 | app, 27 | async_mode="eventlet", 28 | cors_allowed_origins="*" 29 | ) 30 | 31 | from . import models 32 | from . import oauth 33 | from . import socket 34 | 35 | @app.route("/") 36 | def index(): 37 | if not current_user.is_authenticated: 38 | return redirect(url_for("login")) 39 | return redirect(url_for("recognition")) 40 | 41 | @app.route("/recognition") 42 | def recognition(): 43 | if not current_user.is_authenticated: 44 | return redirect(url_for("login")) 45 | return render_template("recognition.html") 46 | 47 | @app.route("/overlay") 48 | def overlay(): 49 | return render_template("overlay.html") 50 | 51 | @app.route("/overlay-sans") 52 | def overlay_sans(): 53 | return render_template("overlay-sans.html") 54 | 55 | -------------------------------------------------------------------------------- /backend/flask/app/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | class Config(object): 5 | SECRET_KEY = os.environ.get("SECRET_KEY") 6 | 7 | migration_directory = "migrations" 8 | SQLALCHEMY_TRACK_MODIFICATIONS = False 9 | SQLALCHEMY_DATABASE_URI = f'postgresql+psycopg2://{os.environ.get("POSTGRES_USER")}:{os.environ.get("POSTGRES_PASSWORD")}@db/{os.environ.get("POSTGRES_DB")}' 10 | 11 | SERVER_PROTOCOL = os.environ.get("SERVER_PROTOCOL") 12 | SERVER_HOSTNAME = os.environ.get("SERVER_HOSTNAME") 13 | 14 | SERVER_HOME = f"{SERVER_PROTOCOL}://{SERVER_HOSTNAME}/" 15 | 16 | TWITCH_CLIENT_ID = os.environ.get("TWITCH_CLIENT_ID") 17 | TWITCH_CLIENT_SECRET = os.environ.get("TWITCH_CLIENT_SECRET") 18 | -------------------------------------------------------------------------------- /backend/flask/app/models.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from flask_dance.consumer.storage.sqla import OAuthConsumerMixin 3 | from sqlalchemy.orm.collections import attribute_mapped_collection 4 | from flask_login import LoginManager, UserMixin 5 | 6 | from . import db, login_manager 7 | 8 | 9 | class SerializableMixin(object): 10 | def _serialize(self): 11 | """Jsonify the sql alchemy query result.""" 12 | convert = dict() 13 | d = dict() 14 | # noinspection PyUnresolvedReferences 15 | for c in self.__class__.__table__.columns: 16 | v = getattr(self, c.name) 17 | if c.type in convert.keys() and v is not None: 18 | try: 19 | d[c.name] = convert[c.type](v) 20 | except: 21 | d[c.name] = "Error: Failed to covert using ", str(convert[c.type]) 22 | elif v is None: 23 | if ( 24 | hasattr(c.type, "__visit_name__") 25 | and c.type.__visit_name__ == "JSON" 26 | ): 27 | d[c.name] = None 28 | elif "INTEGER" == str(c.type) or "NUMERIC" == str(c.type): 29 | # print "??" 30 | d[c.name] = 0 31 | elif "DATETIME" == str(c.type): 32 | d[c.name] = None 33 | else: 34 | # print c.type 35 | d[c.name] = str() 36 | elif isinstance(v, datetime): 37 | if v.utcoffset() is not None: 38 | v = v - v.utcoffset() 39 | d[c.name] = v.strftime("%Y-%m-%d %H:%M:%S") 40 | else: 41 | d[c.name] = v 42 | return d 43 | 44 | def json(self): 45 | raise NotImplementedError() 46 | 47 | class User(UserMixin, SerializableMixin, db.Model): 48 | __tablename__ = "user" 49 | id = db.Column(db.Integer, primary_key=True) 50 | twitch_id = db.Column(db.String(120), nullable=False) 51 | 52 | def json(self): 53 | data = self._serialize() 54 | return data 55 | 56 | 57 | class OAuth(OAuthConsumerMixin, db.Model): 58 | __tablename__ = "oauth" 59 | provider_user_id = db.Column(db.String(256), unique=True, nullable=False) 60 | user_id = db.Column(db.Integer, db.ForeignKey(User.id), nullable=False) 61 | user = db.relationship(User, backref='oauth') 62 | 63 | 64 | @login_manager.user_loader 65 | def load_user(user_id): 66 | return User.query.get(int(user_id)) -------------------------------------------------------------------------------- /backend/flask/app/oauth.py: -------------------------------------------------------------------------------- 1 | from flask import redirect, url_for, session, flash 2 | from flask_login import current_user, login_user, login_required, logout_user 3 | from flask_dance import OAuth2ConsumerBlueprint 4 | from flask_dance.consumer import oauth_authorized, oauth_error 5 | from flask_dance.consumer.storage.sqla import SQLAlchemyStorage 6 | from sqlalchemy.orm.exc import NoResultFound 7 | 8 | from oauthlib.oauth2.rfc6749.errors import TokenExpiredError 9 | from oauthlib.oauth2.rfc6749.clients.web_application import WebApplicationClient 10 | 11 | from . import app, db, login_manager 12 | from .models import User, OAuth 13 | 14 | class TwitchWebClient(WebApplicationClient): 15 | def _add_bearer_token( 16 | self, uri, http_method="GET", body=None, headers=None, token_placement=None 17 | ): 18 | uri, headers, body = super()._add_bearer_token(uri, http_method, body, headers, token_placement) 19 | headers = headers or {} 20 | headers["Client-ID"] = self.client_id 21 | return uri, headers, body 22 | 23 | twitch_blueprint = OAuth2ConsumerBlueprint( 24 | "twitch", 25 | __name__, 26 | client=TwitchWebClient(app.config.get("TWITCH_CLIENT_ID")), 27 | client_id=app.config.get("TWITCH_CLIENT_ID"), 28 | client_secret=app.config.get("TWITCH_CLIENT_SECRET"), 29 | base_url="https://api.twitch.tv/helix/", 30 | token_url="https://id.twitch.tv/oauth2/token", 31 | authorization_url="https://id.twitch.tv/oauth2/authorize", 32 | redirect_url=app.config.get('SERVER_HOME'), 33 | token_url_params={"include_client_id": True}, 34 | scope=("user:read:email",), 35 | storage=SQLAlchemyStorage(OAuth, db.session, user=current_user), 36 | ) 37 | 38 | app.register_blueprint(twitch_blueprint, url_prefix="/login") 39 | 40 | class NotAuthorizedError(Exception): 41 | pass 42 | 43 | login_manager.login_view = "twitch.login" 44 | 45 | @oauth_authorized.connect_via(twitch_blueprint) 46 | def twitch_logged_in(blueprint, token): 47 | if not token: 48 | flash("로그인을 할 수 없습니다.", category="error") 49 | return False 50 | 51 | tb_users = blueprint.session.get("users") 52 | if not tb_users.ok: 53 | flash("사용자 정보를 가져올 수 없습니다.", category="error") 54 | return False 55 | 56 | tb_user = tb_users.json().get('data')[0] 57 | user_id = tb_user.get("id") 58 | 59 | query = OAuth.query.filter_by(provider=blueprint.name, provider_user_id=user_id) 60 | try: 61 | oauth = query.one() 62 | except NoResultFound: 63 | oauth = OAuth(provider=blueprint.name, provider_user_id=user_id, token=token) 64 | 65 | if not oauth.user: 66 | user = User(twitch_id=user_id) 67 | oauth.user = user 68 | db.session.add_all([user, oauth]) 69 | db.session.commit() 70 | else: 71 | oauth.user.twitch_id = user_id 72 | db.session.commit() 73 | 74 | login_user(oauth.user) 75 | flash("성공적으로 로그인했습니다.") 76 | 77 | return False 78 | 79 | @oauth_error.connect_via(twitch_blueprint) 80 | def twitch_error(blueprint, error, error_description=None, error_uri=None): 81 | print('twitch_error') 82 | msg = ("OAuth error from {name}! " "error_description={error_description} error_uri={error_uri}").format( 83 | name=blueprint.name, error_description=error_description, error_uri=error_uri 84 | ) 85 | flash(msg, category="error") 86 | 87 | @app.route("/login") 88 | def login(): 89 | return redirect(url_for("twitch.login")) 90 | 91 | @app.route("/logout") 92 | @login_required 93 | def logout(): 94 | if twitch_blueprint.token: 95 | print(twitch_blueprint.token) 96 | token = twitch_blueprint.token["access_token"] 97 | try: 98 | resp = twitch_blueprint.session.post( 99 | "https://id.twitch.tv/oauth2/revoke", 100 | params={"client_id": twitch_blueprint.client_id, "token": token} 101 | ) 102 | except TokenExpiredError: 103 | pass 104 | del twitch_blueprint.token 105 | logout_user() 106 | return redirect(app.config.get('SERVER_HOME')) 107 | 108 | -------------------------------------------------------------------------------- /backend/flask/app/socket.py: -------------------------------------------------------------------------------- 1 | from flask import session 2 | from flask_login import current_user 3 | from flask_socketio import send, ConnectionRefusedError 4 | 5 | from . import socketio 6 | from . import oauth 7 | 8 | @socketio.on('connect', namespace='/recognition') 9 | def connect_handler(): 10 | if not current_user.is_authenticated: 11 | raise ConnectionRefusedError('unauthorized') 12 | 13 | 14 | @socketio.on('message', namespace='/recognition') 15 | def handle_message(message): 16 | if current_user.twitch_id: 17 | send(message, json=True, broadcast=True, namespace=f'/overlay/{current_user.twitch_id}') -------------------------------------------------------------------------------- /backend/flask/debug.py: -------------------------------------------------------------------------------- 1 | # noinspection PyUnresolvedReferences 2 | from app import socketio, app 3 | 4 | if __name__ == '__main__': 5 | socketio.run(app, host="0.0.0.0", port=80, debug=True) 6 | -------------------------------------------------------------------------------- /backend/flask/manage.py: -------------------------------------------------------------------------------- 1 | # noinspection PyUnresolvedReferences 2 | from app import app, db 3 | from flask_migrate import MigrateCommand 4 | from flask_script import Manager 5 | 6 | manager = Manager(app) 7 | manager.add_command('db', MigrateCommand) 8 | 9 | @manager.command 10 | def drop_all(): 11 | db.drop_all() 12 | 13 | 14 | if __name__ == '__main__': 15 | manager.run() 16 | -------------------------------------------------------------------------------- /backend/flask/migrations/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /backend/flask/migrations/alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # template used to generate migration files 5 | # file_template = %%(rev)s_%%(slug)s 6 | 7 | # set to 'true' to run the environment during 8 | # the 'revision' command, regardless of autogenerate 9 | # revision_environment = false 10 | 11 | 12 | # Logging configuration 13 | [loggers] 14 | keys = root,sqlalchemy,alembic 15 | 16 | [handlers] 17 | keys = console 18 | 19 | [formatters] 20 | keys = generic 21 | 22 | [logger_root] 23 | level = WARN 24 | handlers = console 25 | qualname = 26 | 27 | [logger_sqlalchemy] 28 | level = WARN 29 | handlers = 30 | qualname = sqlalchemy.engine 31 | 32 | [logger_alembic] 33 | level = INFO 34 | handlers = 35 | qualname = alembic 36 | 37 | [handler_console] 38 | class = StreamHandler 39 | args = (sys.stderr,) 40 | level = NOTSET 41 | formatter = generic 42 | 43 | [formatter_generic] 44 | format = %(levelname)-5.5s [%(name)s] %(message)s 45 | datefmt = %H:%M:%S 46 | -------------------------------------------------------------------------------- /backend/flask/migrations/env.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | 3 | import logging 4 | from logging.config import fileConfig 5 | 6 | from sqlalchemy import engine_from_config 7 | from sqlalchemy import pool 8 | 9 | from alembic import context 10 | 11 | # this is the Alembic Config object, which provides 12 | # access to the values within the .ini file in use. 13 | config = context.config 14 | 15 | # Interpret the config file for Python logging. 16 | # This line sets up loggers basically. 17 | fileConfig(config.config_file_name) 18 | logger = logging.getLogger('alembic.env') 19 | 20 | # add your model's MetaData object here 21 | # for 'autogenerate' support 22 | # from myapp import mymodel 23 | # target_metadata = mymodel.Base.metadata 24 | from flask import current_app 25 | config.set_main_option( 26 | 'sqlalchemy.url', current_app.config.get( 27 | 'SQLALCHEMY_DATABASE_URI').replace('%', '%%')) 28 | target_metadata = current_app.extensions['migrate'].db.metadata 29 | 30 | # other values from the config, defined by the needs of env.py, 31 | # can be acquired: 32 | # my_important_option = config.get_main_option("my_important_option") 33 | # ... etc. 34 | 35 | 36 | def run_migrations_offline(): 37 | """Run migrations in 'offline' mode. 38 | 39 | This configures the context with just a URL 40 | and not an Engine, though an Engine is acceptable 41 | here as well. By skipping the Engine creation 42 | we don't even need a DBAPI to be available. 43 | 44 | Calls to context.execute() here emit the given string to the 45 | script output. 46 | 47 | """ 48 | url = config.get_main_option("sqlalchemy.url") 49 | context.configure( 50 | url=url, target_metadata=target_metadata, literal_binds=True 51 | ) 52 | 53 | with context.begin_transaction(): 54 | context.run_migrations() 55 | 56 | 57 | def run_migrations_online(): 58 | """Run migrations in 'online' mode. 59 | 60 | In this scenario we need to create an Engine 61 | and associate a connection with the context. 62 | 63 | """ 64 | 65 | # this callback is used to prevent an auto-migration from being generated 66 | # when there are no changes to the schema 67 | # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html 68 | def process_revision_directives(context, revision, directives): 69 | if getattr(config.cmd_opts, 'autogenerate', False): 70 | script = directives[0] 71 | if script.upgrade_ops.is_empty(): 72 | directives[:] = [] 73 | logger.info('No changes in schema detected.') 74 | 75 | connectable = engine_from_config( 76 | config.get_section(config.config_ini_section), 77 | prefix='sqlalchemy.', 78 | poolclass=pool.NullPool, 79 | ) 80 | 81 | with connectable.connect() as connection: 82 | context.configure( 83 | connection=connection, 84 | target_metadata=target_metadata, 85 | process_revision_directives=process_revision_directives, 86 | **current_app.extensions['migrate'].configure_args 87 | ) 88 | 89 | with context.begin_transaction(): 90 | context.run_migrations() 91 | 92 | 93 | if context.is_offline_mode(): 94 | run_migrations_offline() 95 | else: 96 | run_migrations_online() 97 | -------------------------------------------------------------------------------- /backend/flask/migrations/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | import sqlalchemy_utils 11 | ${imports if imports else ""} 12 | 13 | # revision identifiers, used by Alembic. 14 | revision = ${repr(up_revision)} 15 | down_revision = ${repr(down_revision)} 16 | branch_labels = ${repr(branch_labels)} 17 | depends_on = ${repr(depends_on)} 18 | 19 | 20 | def upgrade(): 21 | ${upgrades if upgrades else "pass"} 22 | 23 | 24 | def downgrade(): 25 | ${downgrades if downgrades else "pass"} 26 | -------------------------------------------------------------------------------- /backend/flask/migrations/versions/59f3082483dd_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 59f3082483dd 4 | Revises: ca9eec44e3f1 5 | Create Date: 2019-09-21 15:52:11.556530 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | import sqlalchemy_utils 11 | 12 | 13 | # revision identifiers, used by Alembic. 14 | revision = '59f3082483dd' 15 | down_revision = 'ca9eec44e3f1' 16 | branch_labels = None 17 | depends_on = None 18 | 19 | 20 | def upgrade(): 21 | # ### commands auto generated by Alembic - please adjust! ### 22 | op.drop_column('user', 'twitch_id') 23 | # ### end Alembic commands ### 24 | 25 | 26 | def downgrade(): 27 | # ### commands auto generated by Alembic - please adjust! ### 28 | op.add_column('user', sa.Column('twitch_id', sa.VARCHAR(length=128), autoincrement=False, nullable=False)) 29 | # ### end Alembic commands ### 30 | -------------------------------------------------------------------------------- /backend/flask/migrations/versions/763fc9f85cf2_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 763fc9f85cf2 4 | Revises: 59f3082483dd 5 | Create Date: 2019-09-21 17:15:08.386984 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | import sqlalchemy_utils 11 | 12 | 13 | # revision identifiers, used by Alembic. 14 | revision = '763fc9f85cf2' 15 | down_revision = '59f3082483dd' 16 | branch_labels = None 17 | depends_on = None 18 | 19 | 20 | def upgrade(): 21 | # ### commands auto generated by Alembic - please adjust! ### 22 | op.add_column('user', sa.Column('twitch_id', sa.String(length=120), nullable=False, server_default='')) 23 | # ### end Alembic commands ### 24 | 25 | 26 | def downgrade(): 27 | # ### commands auto generated by Alembic - please adjust! ### 28 | op.drop_column('user', 'twitch_id') 29 | # ### end Alembic commands ### 30 | -------------------------------------------------------------------------------- /backend/flask/migrations/versions/875e320523d6_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 875e320523d6 4 | Revises: 5 | Create Date: 2019-09-14 13:58:44.365946 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | import sqlalchemy_utils 11 | 12 | 13 | # revision identifiers, used by Alembic. 14 | revision = '875e320523d6' 15 | down_revision = None 16 | branch_labels = None 17 | depends_on = None 18 | 19 | 20 | def upgrade(): 21 | # ### commands auto generated by Alembic - please adjust! ### 22 | op.create_table('user', 23 | sa.Column('provider', sa.String(length=50), nullable=False), 24 | sa.Column('created_at', sa.DateTime(), nullable=False), 25 | sa.Column('token', sqlalchemy_utils.types.json.JSONType(), nullable=False), 26 | sa.Column('id', sa.Integer(), nullable=False), 27 | sa.PrimaryKeyConstraint('id') 28 | ) 29 | # ### end Alembic commands ### 30 | 31 | 32 | def downgrade(): 33 | # ### commands auto generated by Alembic - please adjust! ### 34 | op.drop_table('user') 35 | # ### end Alembic commands ### 36 | -------------------------------------------------------------------------------- /backend/flask/migrations/versions/ca9eec44e3f1_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: ca9eec44e3f1 4 | Revises: 875e320523d6 5 | Create Date: 2019-09-21 15:42:34.689405 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | import sqlalchemy_utils 11 | from sqlalchemy.dialects import postgresql 12 | 13 | # revision identifiers, used by Alembic. 14 | revision = 'ca9eec44e3f1' 15 | down_revision = '875e320523d6' 16 | branch_labels = None 17 | depends_on = None 18 | 19 | 20 | def upgrade(): 21 | # ### commands auto generated by Alembic - please adjust! ### 22 | op.create_table('oauth', 23 | sa.Column('id', sa.Integer(), nullable=False), 24 | sa.Column('provider', sa.String(length=50), nullable=False), 25 | sa.Column('created_at', sa.DateTime(), nullable=False), 26 | sa.Column('token', sqlalchemy_utils.types.json.JSONType(), nullable=False), 27 | sa.Column('provider_user_id', sa.String(length=256), nullable=False), 28 | sa.Column('user_id', sa.Integer(), nullable=False), 29 | sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), 30 | sa.PrimaryKeyConstraint('id'), 31 | sa.UniqueConstraint('provider_user_id') 32 | ) 33 | op.add_column('user', sa.Column('twitch_id', sa.String(length=128), nullable=False)) 34 | op.drop_column('user', 'provider') 35 | op.drop_column('user', 'created_at') 36 | op.drop_column('user', 'token') 37 | # ### end Alembic commands ### 38 | 39 | 40 | def downgrade(): 41 | # ### commands auto generated by Alembic - please adjust! ### 42 | op.add_column('user', sa.Column('token', postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=False)) 43 | op.add_column('user', sa.Column('created_at', postgresql.TIMESTAMP(), autoincrement=False, nullable=False)) 44 | op.add_column('user', sa.Column('provider', sa.VARCHAR(length=50), autoincrement=False, nullable=False)) 45 | op.drop_column('user', 'twitch_id') 46 | op.drop_table('oauth') 47 | # ### end Alembic commands ### 48 | -------------------------------------------------------------------------------- /backend/flask/run.py: -------------------------------------------------------------------------------- 1 | # noinspection PyUnresolvedReferences 2 | from app import socketio, app 3 | 4 | if __name__ == '__main__': 5 | socketio.run(app, host="0.0.0.0", port=80, debug=False) 6 | -------------------------------------------------------------------------------- /docker-compose.example.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | db: 4 | image: postgres:9.6 5 | volumes: 6 | - ./db-data:/var/lib/postgresql/data 7 | restart: unless-stopped 8 | env_file: 9 | - ./settings.env 10 | environment: 11 | - "LC_COLLATE=C" 12 | 13 | backend: 14 | build: ./backend 15 | links: 16 | - db 17 | volumes: 18 | - ./backend/flask:/var/www/flask 19 | - ./frontend:/frontend 20 | restart: unless-stopped 21 | env_file: 22 | - ./settings.env 23 | -------------------------------------------------------------------------------- /frontend/overlay-sans.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Open Captions Overlay - Overlay(Sans) 5 | 6 | 13 | 112 | 113 | 114 | 115 | 116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |

130 |

131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 | 282 | -------------------------------------------------------------------------------- /frontend/overlay.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Open Captions Overlay - Overlay 5 | 6 | 13 | 14 | 121 | 122 | 123 | 124 | 125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |

139 |

140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 | 321 | -------------------------------------------------------------------------------- /frontend/profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yf-dev/OpenCaptionsOverlay/433f5a83e074f8f808b92bf323ce4eee70337496/frontend/profile.png -------------------------------------------------------------------------------- /frontend/recognition.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Open Captions Overlay - Recognition 5 | 6 | 7 | 14 | 15 | 16 | 17 | 97 | 98 | 99 | 100 |
101 |
102 | {% with messages = get_flashed_messages(with_categories=true) %} 103 | {% if messages %} 104 |
105 | {% for category, message in messages %} 106 |
107 | {{ message }} 108 |
109 | {% endfor %} 110 |
111 | {% endif %} 112 | {% endwith %} 113 | 127 |
128 |
129 |
130 |
131 | 132 | 로그인 정보 갱신 133 | 137 | 138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
최근인식결과
146 |
147 |
148 |
149 | 150 | 151 |
152 |
153 |
154 |
155 |
156 |
157 | 363 | -------------------------------------------------------------------------------- /frontend/sans.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yf-dev/OpenCaptionsOverlay/433f5a83e074f8f808b92bf323ce4eee70337496/frontend/sans.png -------------------------------------------------------------------------------- /frontend/voice_sans.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yf-dev/OpenCaptionsOverlay/433f5a83e074f8f808b92bf323ce4eee70337496/frontend/voice_sans.mp3 -------------------------------------------------------------------------------- /sample.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yf-dev/OpenCaptionsOverlay/433f5a83e074f8f808b92bf323ce4eee70337496/sample.gif -------------------------------------------------------------------------------- /settings.env.example: -------------------------------------------------------------------------------- 1 | SECRET_KEY=randomsecretkey 2 | API_HOSTNAME=localhost:8088 3 | SERVER_PROTOCOL=http 4 | SERVER_PORT=8088 5 | SERVER_HOSTNAME=localhost 6 | POSTGRES_PASSWORD=passwordpassworpasswopasswpasspaspap 7 | POSTGRES_DB=open-captions-overlay 8 | POSTGRES_USER=cco 9 | TWITCH_CLIENT_ID=clientid 10 | TWITCH_CLIENT_SECRET=clientsecret --------------------------------------------------------------------------------