├── .gitignore ├── LICENSE.md ├── Logo Raw └── Moars Logo.ai ├── Makefile.template ├── README.md ├── __init__.py ├── requirements.txt └── smol ├── manage.py ├── smol ├── __init__.py ├── asgi.py ├── settings.py ├── urls.py └── wsgi.py └── viewer ├── __init__.py ├── apps.py ├── context_processors.py ├── forms.py ├── management ├── __init__.py └── commands │ ├── __init__.py │ ├── analyze_all.py │ ├── cleanup_recognition_data.py │ └── update_videos.py ├── migrations ├── .gitignore └── __init__.py ├── models.py ├── static └── viewer │ ├── css │ ├── font-awesome.css │ ├── pico.custom.css │ └── pico.fuchsia.css │ ├── favicons │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── browserconfig.xml │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── mstile-150x150.png │ ├── safari-pinned-tab.svg │ └── site.webmanifest │ ├── fonts │ ├── FontAwesome.otf │ ├── fontawesome-webfont.eot │ ├── fontawesome-webfont.svg │ ├── fontawesome-webfont.ttf │ ├── fontawesome-webfont.woff │ └── fontawesome-webfont.woff2 │ ├── js │ ├── filter.js │ ├── humanize.min.js │ ├── library_load.js │ └── video_play.js │ ├── less │ ├── animated.less │ ├── bordered-pulled.less │ ├── core.less │ ├── fixed-width.less │ ├── font-awesome.less │ ├── icons.less │ ├── larger.less │ ├── list.less │ ├── mixins.less │ ├── path.less │ ├── rotated-flipped.less │ ├── screen-reader.less │ ├── stacked.less │ └── variables.less │ └── scss │ ├── _animated.scss │ ├── _bordered-pulled.scss │ ├── _core.scss │ ├── _fixed-width.scss │ ├── _icons.scss │ ├── _larger.scss │ ├── _list.scss │ ├── _mixins.scss │ ├── _path.scss │ ├── _rotated-flipped.scss │ ├── _screen-reader.scss │ ├── _stacked.scss │ ├── _variables.scss │ └── font-awesome.scss ├── templates └── viewer │ ├── base.html │ ├── index.html │ ├── labels.html │ ├── loader.html │ ├── search.html │ ├── video.html │ ├── video_gallery.html │ └── videos_page.html ├── templatetags ├── __init__.py └── custom_filters.py ├── tests.py ├── urls.py ├── utils.py ├── video_processor.py └── views.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | docker-compose.yml 131 | *.Zone.Identifier 132 | 133 | smol.db 134 | .venv 135 | **/viewer/images 136 | **smol/local_media 137 | 138 | Makefile -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published 637 | by the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . -------------------------------------------------------------------------------- /Logo Raw/Moars Logo.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EinAeffchen/Smol/2ee58c4647973818bb3c124f70a92b71e3b7f5c0/Logo Raw/Moars Logo.ai -------------------------------------------------------------------------------- /Makefile.template: -------------------------------------------------------------------------------- 1 | kill: 2 | docker-compose down 3 | docker-compose rm -f 4 | 5 | up: 6 | cd smol && python3 manage.py collectstatic --noinput 7 | cd smol && python3 manage.py makemigrations 8 | cd smol && python3 manage.py migrate 9 | cd smol && python3 manage.py runserver 0.0.0.0:8080 10 | 11 | analyze: 12 | cd smol && python3 manage.py analyze_all 13 | 14 | logs: 15 | docker logs django 16 | 17 | db_repair: 18 | cd smol/local_media/.smol/db && echo ".recover" |sqlite3 smol.db|sqlite3 repaired_smol.db 19 | && mv smol.db corrupt_smol.db 20 | && mv repaired_smol.db smol.db 21 | 22 | down: 23 | docker-compose -p smol down 24 | 25 | killall: 26 | docker kill django nginx postgresql 27 | docker rm django nginx postgresql 28 | docker volume rm smol_db-data smol_media-volume smol_ml-volume smol_static-movie-volume smol_static-volume 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SMOL 2 | 3 | Small Media Organization System 4 | 5 | This is a very rudimentary setup only meant for local hosting. 6 | 7 | ## Contribute 8 | 9 | - Fork and create a new branch, give it an appropriate name and submit a pull request. 10 | - Post Ideas, bugs, wishes, etc. as issues please. 11 | 12 | ## Get started 13 | 14 | ### Setup 15 | * Rename Makefile.template to Makefile 16 | * Run `pip install -r requirements.txt` 17 | * Either link your local files to smol/local_media or change the `MEDIA_ROOT` in [settings.py](smol/smol/settings.py). 18 | * Run first time analysis using `make analyze` 19 | * Start server with `make up` 20 | * Open browser: [Server](http://localhost:8080) 21 | 22 | 23 | ## Licensed under: 24 | [GNU AFFERO GENERAL PUBLIC LICENSE](./LICENSE.md) -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | from smol.utils import * 2 | from smol.model import * -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | django==5.0.6 2 | django-debug-toolbar==4.4.2 3 | ffmpeg-python==0.2.0 4 | gunicorn==22.0.0 5 | Pillow==10.3.0 6 | ffprobe-python==1.0.3 7 | opencv-python==4.11.0.86 8 | deepface==0.0.92 9 | facenet-pytorch==2.6.0 10 | tf-keras==2.18.0 -------------------------------------------------------------------------------- /smol/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'smol.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /smol/smol/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EinAeffchen/Smol/2ee58c4647973818bb3c124f70a92b71e3b7f5c0/smol/smol/__init__.py -------------------------------------------------------------------------------- /smol/smol/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for smol project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'smol.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /smol/smol/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for website project. 3 | 4 | Generated by 'django-admin startproject' using Django 2.2.1. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/2.2/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3" 16 | from pathlib import Path 17 | 18 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 19 | # BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 20 | BASE_DIR = Path(__file__).resolve().parent.parent 21 | 22 | # Quick-start development settings - unsuitable for production 23 | # See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/ 24 | 25 | # SECURITY WARNING: keep the secret key used in production secret! 26 | SECRET_KEY = "k&ndet-+-&(yxp_hih^k9y*$hbe2p=$vzeryfj$x*&$#+0-$sx" 27 | 28 | # SECURITY WARNING: don't run with debug turned on in production! 29 | DEBUG = True 30 | 31 | ALLOWED_HOSTS = [ 32 | "192.168.178.20", 33 | "localhost", 34 | "127.0.0.1", 35 | os.environ.get("HOST", "smol.localhost"), 36 | ] 37 | DEFAULT_CHARSET = "utf-8" 38 | 39 | 40 | # Application definition 41 | 42 | INSTALLED_APPS = [ 43 | "viewer.apps.ViewerConfig", 44 | # "django.contrib.admin", 45 | "django.contrib.auth", 46 | "django.contrib.contenttypes", 47 | "django.contrib.sessions", 48 | "django.contrib.messages", 49 | "django.contrib.staticfiles", 50 | "django.contrib.humanize", 51 | # 'corsheaders', 52 | "debug_toolbar", 53 | ] 54 | 55 | MIDDLEWARE = [ 56 | # 'django.middleware.cache.UpdateCacheMiddleware', 57 | "debug_toolbar.middleware.DebugToolbarMiddleware", 58 | "django.contrib.sessions.middleware.SessionMiddleware", 59 | "django.middleware.security.SecurityMiddleware", 60 | "django.middleware.common.CommonMiddleware", 61 | # 'django.middleware.csrf.CsrfViewMiddleware', 62 | "django.contrib.auth.middleware.AuthenticationMiddleware", 63 | "django.contrib.messages.middleware.MessageMiddleware", 64 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 65 | # 'django.middleware.cache.FetchFromCacheMiddleware' 66 | ] 67 | 68 | CORS_ORIGIN_ALLOW_ALL = True 69 | CORS_ALLOW_CREDENTIALS = False 70 | ROOT_URLCONF = "smol.urls" 71 | 72 | 73 | TEMPLATES = [ 74 | { 75 | "BACKEND": "django.template.backends.django.DjangoTemplates", 76 | "DIRS": [], 77 | "APP_DIRS": True, 78 | "OPTIONS": { 79 | "context_processors": [ 80 | "viewer.context_processors.random_video", 81 | "django.template.context_processors.debug", 82 | "django.template.context_processors.request", 83 | "django.contrib.auth.context_processors.auth", 84 | "django.contrib.messages.context_processors.messages", 85 | ], 86 | }, 87 | }, 88 | ] 89 | 90 | WSGI_APPLICATION = "smol.wsgi.application" 91 | 92 | 93 | # Password validation 94 | # https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators 95 | 96 | AUTH_PASSWORD_VALIDATORS = [ 97 | { 98 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", 99 | }, 100 | { 101 | "NAME": ( 102 | "django.contrib.auth.password_validation.MinimumLengthValidator" 103 | ), 104 | }, 105 | { 106 | "NAME": ( 107 | "django.contrib.auth.password_validation.CommonPasswordValidator" 108 | ), 109 | }, 110 | { 111 | "NAME": ( 112 | "django.contrib.auth.password_validation.NumericPasswordValidator" 113 | ), 114 | }, 115 | ] 116 | 117 | 118 | # Internationalization 119 | # https://docs.djangoproject.com/en/2.2/topics/i18n/ 120 | 121 | LANGUAGE_CODE = "en-us" 122 | 123 | TIME_ZONE = "CET" 124 | 125 | USE_I18N = True 126 | 127 | USE_L10N = True 128 | 129 | USE_TZ = True 130 | 131 | 132 | # Static files (CSS, JavaScript, Images) 133 | # https://docs.djangoproject.com/en/2.2/howto/static-files/ 134 | 135 | PROJECT_ROOT = Path(__file__).resolve().parent 136 | PROJECT_ROOT_alt = os.path.dirname(os.path.abspath(__file__)) 137 | 138 | 139 | def always_true(request): 140 | return True 141 | 142 | 143 | SHOW_TOOLBAR_CALLBACK = always_true 144 | INTERNAL_IPS = [ 145 | "127.0.0.1", 146 | "localhost", 147 | "192.168.178.20", 148 | "192.168.178.20", 149 | os.environ.get("HOST", "smol.localhost"), 150 | ] 151 | 152 | SITE_ID = 1 153 | 154 | PREVIEW_IMAGES = 15 155 | 156 | MEDIA_URL = "/viewer/images/" 157 | MEDIA_ROOT = BASE_DIR / "local_media/" 158 | MEDIA_DIR = MEDIA_ROOT 159 | 160 | STATIC_URL = "static/" 161 | STATIC_ROOT = MEDIA_DIR / ".smol/static" 162 | STATICFILES_DIRS = (MEDIA_DIR / ".smol/static/viewer/images",) 163 | # Database 164 | # https://docs.djangoproject.com/en/2.2/ref/settings/#databases 165 | 166 | sqlite_file = MEDIA_DIR / ".smol/db" 167 | sqlite_file.mkdir(exist_ok=True, parents=True) 168 | sqlite_file = sqlite_file / "smol.db" 169 | 170 | RECOGNITION_DATA_PATH = MEDIA_DIR / ".smol/static/faces" 171 | RECOGNITION_DATA_PATH.mkdir(parents=True, exist_ok=True) 172 | 173 | 174 | DATABASES = { 175 | "default": { 176 | "ENGINE": "django.db.backends.sqlite3", 177 | "NAME": str(sqlite_file), 178 | } 179 | } 180 | 181 | THUMBNAIL_DIR = STATIC_ROOT / "viewer/images/thumbnails" 182 | PREVIEW_DIR = STATIC_ROOT / "viewer/images/previews" 183 | 184 | if not THUMBNAIL_DIR.is_dir(): 185 | THUMBNAIL_DIR.mkdir(exist_ok=True, parents=True) 186 | if not PREVIEW_DIR.is_dir(): 187 | PREVIEW_DIR.mkdir(exist_ok=True, parents=True) 188 | 189 | VIDEO_SUFFIXES = [ 190 | ".mp4", 191 | ".mov", 192 | ".wmv", 193 | ".avi", 194 | ".flv", 195 | ".mkv", 196 | ".webm", 197 | ".gp3", 198 | ".ts", 199 | ".mpeg", 200 | ] 201 | IMAGE_SUFFIXES = [ 202 | ".jpg", 203 | ".jpeg", 204 | ".png", 205 | ".JPG", 206 | ".tiff", 207 | ".gif", 208 | ".bmp", 209 | ] 210 | 211 | # face recognition settings 212 | 213 | RECOGNITION_FRAME_SKIP = os.getenv( 214 | "RECOGNITION_RECOGNITION_FRAME_SKIP", 10 215 | ) # steps in frames used to check for faces 216 | RECOGNITION_FACE_COUNT = os.getenv("RECOGNITION_FACE_COUNT", 3) 217 | # For other models and detector backends check: https://github.com/serengil/deepface?tab=readme-ov-file 218 | RECOGNITION_MODEL = os.getenv("RECOGNITION_MODEL", "Facenet512") 219 | RECOGNITION_IMAGE_NORMALIZATION = os.getenv( 220 | "RECOGNITION_IMAGE_NORMALIZATION", "Facenet2018" 221 | ) 222 | RECOGNITION_DETECTION_BACKEND = os.getenv( 223 | "RECOGNITION_DETECTION_BACKEND", "ssd" 224 | ) 225 | RECOGNITION_STORE_FACES = os.getenv("RECOGNITION_STORE_FACES", True) 226 | RECOGNITION_THRESHOLD = os.getenv("RECOGNITION_THRESHOLD",0.25) 227 | -------------------------------------------------------------------------------- /smol/smol/urls.py: -------------------------------------------------------------------------------- 1 | """smol URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/3.2/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.urls import path, include 17 | from django.conf import settings 18 | from django.conf.urls.static import static 19 | import debug_toolbar 20 | 21 | urlpatterns = [ 22 | path("", include(("viewer.urls", "viewer"), namespace="viewer")), 23 | path("__debug__/", include(debug_toolbar.urls)), 24 | ] 25 | if settings.DEBUG: 26 | # urlpatterns += staticfiles_urlpatterns() 27 | # urlpatterns += [ 28 | # re_path(r"^static/(?P.*)$", views.serve), 29 | # ] 30 | urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) 31 | urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 32 | -------------------------------------------------------------------------------- /smol/smol/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for smol project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'smol.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /smol/viewer/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EinAeffchen/Smol/2ee58c4647973818bb3c124f70a92b71e3b7f5c0/smol/viewer/__init__.py -------------------------------------------------------------------------------- /smol/viewer/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ViewerConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'viewer' 7 | -------------------------------------------------------------------------------- /smol/viewer/context_processors.py: -------------------------------------------------------------------------------- 1 | from .models import Video 2 | import random 3 | 4 | 5 | def random_video(request): 6 | rvideos = Video.objects.order_by('?')[:33] 7 | if rvideos: 8 | rvideo = random.choice(rvideos) 9 | else: 10 | rvideo = None 11 | return {"rvideo": rvideo, "rvideos": rvideos} 12 | -------------------------------------------------------------------------------- /smol/viewer/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from .models import Person, Video, Label 3 | 4 | 5 | class FilterForm(forms.Form): 6 | FILTERS = [ 7 | ("", "Select Order"), 8 | ("-favorite", "favorites"), 9 | ("dim_width", "resolution asc"), 10 | ("-dim_width", "resolution desc"), 11 | ("-inserted_at", "newest"), 12 | ("inserted_at", "oldest"), 13 | ("duration", "shortest"), 14 | ("-duration", "longest"), 15 | ] 16 | labels = Label.objects.all() 17 | order_videos = forms.ChoiceField(choices=FILTERS, required=False, initial=0) 18 | label_field = forms.ModelMultipleChoiceField( 19 | queryset=labels, required=False 20 | ) 21 | 22 | # def __init__(self, *args, **kwargs): 23 | # super(FilterForm, self).__init__(*args, **kwargs) 24 | # print("FORM: %s", dir(self)) 25 | # self.declared_fields['labels'].widget.attrs['class'] = 'form-control' 26 | 27 | 28 | class LabelForm(forms.Form): 29 | labels = forms.ModelMultipleChoiceField(queryset=Label.objects.all()) 30 | 31 | class LabelAddForm(forms.Form): 32 | labels = forms.CharField() 33 | 34 | 35 | class ImageForm(forms.ModelForm): 36 | """Form for the image model""" 37 | 38 | class Meta: 39 | model = Person 40 | fields = ("avatar",) 41 | 42 | 43 | class DeletePersonForm(forms.ModelForm): 44 | class Meta: 45 | model = Person 46 | fields = ("id",) 47 | 48 | 49 | class OrderedModelMultipleChoiceField(forms.ModelMultipleChoiceField): 50 | def clean(self, value): 51 | qs = super(OrderedModelMultipleChoiceField, self).clean(value) 52 | return qs 53 | 54 | 55 | class PersonForm(forms.ModelForm): 56 | """Form for the image model""" 57 | 58 | videos = OrderedModelMultipleChoiceField( 59 | Video.objects.order_by("filename") 60 | ) 61 | 62 | class Meta: 63 | model = Person 64 | fields = ( 65 | "forename", 66 | "surname", 67 | "birth_year", 68 | "nationality", 69 | "labels", 70 | "videos", 71 | "images", 72 | "avatar", 73 | ) 74 | -------------------------------------------------------------------------------- /smol/viewer/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EinAeffchen/Smol/2ee58c4647973818bb3c124f70a92b71e3b7f5c0/smol/viewer/management/__init__.py -------------------------------------------------------------------------------- /smol/viewer/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EinAeffchen/Smol/2ee58c4647973818bb3c124f70a92b71e3b7f5c0/smol/viewer/management/commands/__init__.py -------------------------------------------------------------------------------- /smol/viewer/management/commands/analyze_all.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | from viewer.models import Video 3 | from viewer.video_processor import recognize_faces, add_labels_by_path 4 | from tqdm import tqdm 5 | 6 | 7 | class Command(BaseCommand): 8 | def handle(self, *args, **options): 9 | for video in tqdm(Video.objects.all()): 10 | add_labels_by_path(video) 11 | video.face_encodings 12 | print("Finished creating encodings! Starting Recognition") 13 | for video in tqdm(Video.objects.all()): 14 | if not video.ran_recognition: 15 | recognize_faces(video) 16 | video.ran_recognition = True 17 | -------------------------------------------------------------------------------- /smol/viewer/management/commands/cleanup_recognition_data.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | from viewer.models import Video 3 | from tqdm import tqdm 4 | 5 | 6 | class Command(BaseCommand): 7 | def handle(self, *args, **options): 8 | for video in tqdm(Video.objects.all()): 9 | video.delete_encoding() 10 | video.ran_recognition = False 11 | -------------------------------------------------------------------------------- /smol/viewer/management/commands/update_videos.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | from viewer.models import Video 3 | from tqdm import tqdm 4 | 5 | 6 | class Command(BaseCommand): 7 | def handle(self, *args, **options): 8 | video:Video 9 | for video in tqdm(Video.objects.all()): 10 | video.extracted_faces = True 11 | video.save() 12 | -------------------------------------------------------------------------------- /smol/viewer/migrations/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | !__init__.py -------------------------------------------------------------------------------- /smol/viewer/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EinAeffchen/Smol/2ee58c4647973818bb3c124f70a92b71e3b7f5c0/smol/viewer/migrations/__init__.py -------------------------------------------------------------------------------- /smol/viewer/models.py: -------------------------------------------------------------------------------- 1 | import pickle 2 | from pathlib import Path 3 | 4 | import cv2 5 | import django 6 | from deepface.modules import detection, representation 7 | from django.conf import settings 8 | from django.db import models 9 | from PIL import Image as PILImage 10 | 11 | 12 | class Label(models.Model): 13 | label = models.TextField(unique=True) 14 | 15 | class Meta: 16 | ordering = ["label"] 17 | 18 | def __unicode__(self): 19 | return f"{self.label}" 20 | 21 | def __str__(self): 22 | return f"{self.label}" 23 | 24 | 25 | class Video(models.Model): 26 | 27 | class Meta: 28 | indexes = [ 29 | models.Index(fields=["filename"]), 30 | models.Index(fields=["path"]), 31 | ] 32 | 33 | id = models.IntegerField(primary_key=True) 34 | id_hash = models.TextField(unique=True, null=True) 35 | path = models.TextField(unique=True) 36 | filename = models.TextField() 37 | dim_height = models.IntegerField(null=True) 38 | dim_width = models.IntegerField(null=True) 39 | duration = models.FloatField() 40 | audiocodec = models.TextField(null=True) 41 | bitrate = models.BigIntegerField(null=True) 42 | size = models.BigIntegerField() 43 | videocodec = models.TextField(null=True) 44 | preview = models.TextField() 45 | thumbnail = models.TextField() 46 | processed = models.BooleanField(default=False) 47 | favorite = models.BooleanField(default=False) 48 | labels = models.ManyToManyField(Label) 49 | inserted_at = models.DateTimeField(default=django.utils.timezone.now) 50 | related_videos = models.ManyToManyField( 51 | "self", 52 | verbose_name="related videos", 53 | symmetrical=True, 54 | blank=True, 55 | through="VideoPersonMatch", 56 | ) 57 | extracted_faces = models.BooleanField(default=False) 58 | ran_recognition = models.BooleanField(default=False) 59 | 60 | def __str__(self): 61 | return f"{self.filename}" 62 | 63 | def _delete_previews(self): 64 | thumbnail = settings.THUMBNAIL_DIR / f"{self.id_hash}.jpg" 65 | if thumbnail.is_file(): 66 | thumbnail.unlink() 67 | print(f"deleted {thumbnail}") 68 | 69 | def delete_full(self): 70 | obj = Path(self.path) 71 | print(f"Deleting {self.id_hash}...") 72 | try: 73 | obj.unlink() 74 | except (PermissionError, FileNotFoundError) as e: 75 | print("Couldn't delete, file busy or already deleted.") 76 | self._delete_previews() 77 | self.delete() 78 | 79 | def delete_entry(self): 80 | self._delete_previews() 81 | self.delete() 82 | print("DELETED") 83 | 84 | def file_exists(self): 85 | obj = Path(self.path) 86 | if obj.is_file(): 87 | return True 88 | else: 89 | return False 90 | 91 | def clean(self): 92 | if not self.file_exists(): 93 | self.delete() 94 | return 1 95 | return 0 96 | 97 | @property 98 | def full_path(self) -> Path: 99 | return settings.MEDIA_DIR / self.path 100 | 101 | def delete_encoding(self): 102 | if self.face_encoding_file.is_file(): 103 | self.face_encoding_file.unlink() 104 | [face.unlink() for face in self.examples_faces] 105 | self.ran_recognition = False 106 | self.extracted_faces = False 107 | self.save() 108 | 109 | def create_encodings(self) -> list[dict]: 110 | if self.extracted_faces: 111 | print(f"{self.id} already has embeddings, skipping!") 112 | return 113 | movie = cv2.VideoCapture(self.full_path) 114 | total_frame_count = int(movie.get(cv2.CAP_PROP_FRAME_COUNT)) 115 | frame_number = 0 116 | face_count = 0 117 | full_face_encodings = [] 118 | print("Starting recognition...") 119 | while movie.isOpened(): 120 | ret, frame = movie.read() 121 | 122 | if ( 123 | not ret 124 | or face_count >= settings.RECOGNITION_FACE_COUNT 125 | or frame_number >= total_frame_count 126 | ): 127 | movie.release() 128 | break 129 | if frame.shape[1] <= 640: 130 | frame = cv2.resize(frame, None, fx=2, fy=2) 131 | rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) 132 | try: 133 | img_objs = detection.extract_faces( 134 | rgb_frame, 135 | expand_percentage=100, 136 | detector_backend=settings.RECOGNITION_DETECTION_BACKEND, 137 | ) 138 | except ValueError: 139 | img_objs = [] 140 | 141 | if img_objs: 142 | for img_obj in img_objs: 143 | confidence = img_obj["confidence"] 144 | print(confidence) 145 | if confidence < 0.8: 146 | frame_jump = settings.RECOGNITION_FRAME_SKIP 147 | continue 148 | img_content = img_obj["face"] 149 | img_region = img_obj["facial_area"] 150 | embedding_obj = representation.represent( 151 | img_path=img_content, 152 | model_name=settings.RECOGNITION_MODEL, 153 | detector_backend="skip", 154 | normalization=settings.RECOGNITION_IMAGE_NORMALIZATION, 155 | ) 156 | img_representation = embedding_obj[0]["embedding"] 157 | # get more different kinds of faces 158 | full_face_encodings.append( 159 | { 160 | "identity": f"{self.id_hash}", 161 | "hash": self.id_hash, 162 | "embedding": img_representation, 163 | "target_x": img_region["x"], 164 | "target_y": img_region["y"], 165 | "target_w": img_region["w"], 166 | "target_h": img_region["h"], 167 | } 168 | ) 169 | self.save_face( 170 | face_count, 171 | rgb_frame, 172 | list(img_region.values())[:4], 173 | ) 174 | frame_jump = int(total_frame_count / 10) 175 | face_count += 1 176 | else: 177 | frame_jump = settings.RECOGNITION_FRAME_SKIP 178 | frame_number += frame_jump 179 | movie.set(cv2.CAP_PROP_POS_FRAMES, frame_number) 180 | 181 | if face_count > 0: 182 | with open(self.face_encoding_file, "wb") as f_out: 183 | pickle.dump(full_face_encodings, f_out) 184 | self.extracted_faces = True 185 | self.save() 186 | print("Finished extraction!") 187 | return full_face_encodings 188 | 189 | def save_face( 190 | self, 191 | face_count: int, 192 | rgb_frame: cv2.typing.MatLike, 193 | face_locations: list[int], 194 | ): 195 | if settings.RECOGNITION_STORE_FACES: 196 | HEIGHT, WIDTH = rgb_frame.shape[0:2] 197 | x, y, w, h = face_locations 198 | face_only = rgb_frame[ 199 | max(0, y - 50) : min(y + h + 50, HEIGHT), 200 | max(0, x - 50) : min(x + w + 50, WIDTH), 201 | ] 202 | pil_image = PILImage.fromarray(face_only) 203 | face_folder: Path = self.recognition_data_path / str(self.id_hash) 204 | face_folder.mkdir(exist_ok=True, parents=True) 205 | try: 206 | pil_image.save( 207 | face_folder / f"{self.id_hash}_{face_count}.jpg" 208 | ) 209 | except ValueError: 210 | print("Can't save face as image!") 211 | 212 | @property 213 | def recognition_data_path(self) -> Path: 214 | path: Path = settings.RECOGNITION_DATA_PATH 215 | path.mkdir(exist_ok=True, parents=True) 216 | return path 217 | 218 | @property 219 | def examples_faces(self) -> list[Path]: 220 | return list( 221 | (self.recognition_data_path / str(self.id_hash)).glob("*.jpg") 222 | ) 223 | 224 | @property 225 | def face_encoding_file(self) -> Path: 226 | return self.recognition_data_path / f"{str(self.id_hash)}.pkl" 227 | 228 | @property 229 | def face_encodings(self) -> dict: 230 | if self.face_encoding_file.is_file(): 231 | with open(self.face_encoding_file, "rb") as f_in: 232 | return pickle.load(f_in) 233 | else: 234 | print(f"No Encoding file found for {self.id}. Creating...") 235 | return self.create_encodings() 236 | 237 | 238 | class VideoPersonMatch(models.Model): 239 | class Meta: 240 | ordering = ["-score"] 241 | indexes = [ 242 | models.Index(fields=["score"]), 243 | ] 244 | models.UniqueConstraint( 245 | fields=["source_video", "related_video"], name="unique_relation" 246 | ) 247 | 248 | source_video = models.ForeignKey( 249 | Video, on_delete=models.CASCADE, related_name="from_video" 250 | ) 251 | related_video = models.ForeignKey( 252 | Video, on_delete=models.CASCADE, related_name="to_video" 253 | ) 254 | score = models.FloatField(null=False) 255 | 256 | 257 | class Image(models.Model): 258 | path = models.TextField(unique=True) 259 | filename = models.TextField() 260 | dim_height = models.IntegerField(null=True) 261 | dim_width = models.IntegerField(null=True) 262 | size = models.IntegerField() 263 | processed = models.BooleanField(default=False) 264 | favorite = models.BooleanField(default=False) 265 | inserted_at = models.DateField(default=django.utils.timezone.now) 266 | labels = models.ManyToManyField(Label) 267 | 268 | def delete_full(self): 269 | obj = Path(self.path) 270 | print(f"Deleting {self.id}...") 271 | try: 272 | obj.unlink() 273 | except (PermissionError, FileNotFoundError) as e: 274 | print("Couldn't delete, file busy or already deleted.") 275 | self.delete() 276 | 277 | def clean(self): 278 | if not self.file_exists(): 279 | self.delete() 280 | return 1 281 | return 0 282 | 283 | def file_exists(self): 284 | obj = Path(self.path) 285 | if obj.is_file(): 286 | return True 287 | else: 288 | return False 289 | 290 | def __str__(self): 291 | return f"{self.filename}" 292 | 293 | class Meta: 294 | ordering = ["-inserted_at"] 295 | indexes = [ 296 | models.Index(fields=["path"]), 297 | ] 298 | 299 | 300 | class Person(models.Model): 301 | class Meta: 302 | ordering = ["surname", "forename"] 303 | indexes = [ 304 | models.Index(fields=["forename"]), 305 | models.Index(fields=["surname"]), 306 | models.Index(fields=["function"]), 307 | ] 308 | 309 | forename = models.TextField(null=True) 310 | surname = models.TextField(null=True) 311 | birth_year = models.IntegerField(null=True) 312 | nationality = models.TextField(null=True) 313 | labels = models.ManyToManyField(Label, blank=True) 314 | video_files = models.ManyToManyField(Video, blank=True) 315 | images = models.ManyToManyField(Image, blank=True) 316 | avatar = models.ImageField( 317 | upload_to="images/person_profiles/", null=True, blank=True 318 | ) 319 | function = models.TextField(null=True) 320 | 321 | def delete_full(self): 322 | print(f"Deleting {self.id}...") 323 | self.delete() 324 | 325 | def __str__(self): 326 | return f"{self.forename} {self.surname}" 327 | -------------------------------------------------------------------------------- /smol/viewer/static/viewer/css/pico.custom.css: -------------------------------------------------------------------------------- 1 | .main-container { 2 | line-height: 0; 3 | column-count: 5; 4 | column-gap: 15px; 5 | } 6 | 7 | /* video { 8 | height: auto; 9 | width: 100%; 10 | } */ 11 | 12 | .search-form { 13 | margin-bottom:0px !important; 14 | } 15 | 16 | .video-block { 17 | height: auto; 18 | width: 100%; 19 | break-inside: avoid-column; 20 | } 21 | 22 | .video-block:nth-of-type(2){ 23 | break-after: column; 24 | display: block; 25 | } 26 | 27 | .video-block video { 28 | width: 100%; 29 | } 30 | 31 | .video-fullsize { 32 | max-height: 50vh; 33 | width:100%; 34 | } 35 | 36 | .video-block span { 37 | position: relative; 38 | top: -16px; 39 | color: white; 40 | background-color: black; 41 | text-align: right; 42 | border-radius: .25rem; 43 | align-items: center; 44 | opacity: .5; 45 | padding: .1rem .5rem; 46 | display: inline list-item 47 | } 48 | 49 | .label-group-box { 50 | flex-wrap: wrap; 51 | justify-content: center; 52 | } 53 | 54 | .label-group { 55 | max-height: 3rem; 56 | max-width: 14rem; 57 | min-width: 8rem; 58 | margin: 5px 1rem; 59 | } 60 | 61 | .delete-button { 62 | padding: 10px 5px; 63 | } 64 | 65 | @media only screen and (max-width: 600px) { 66 | .main-container { 67 | column-count: 1; 68 | } 69 | } 70 | 71 | @media only screen and (max-width: 1000px) { 72 | .main-container { 73 | column-count: 3; 74 | } 75 | } 76 | 77 | @media only screen and (max-width: 800px) { 78 | .main-container { 79 | column-count: 3; 80 | } 81 | } -------------------------------------------------------------------------------- /smol/viewer/static/viewer/favicons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EinAeffchen/Smol/2ee58c4647973818bb3c124f70a92b71e3b7f5c0/smol/viewer/static/viewer/favicons/android-chrome-192x192.png -------------------------------------------------------------------------------- /smol/viewer/static/viewer/favicons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EinAeffchen/Smol/2ee58c4647973818bb3c124f70a92b71e3b7f5c0/smol/viewer/static/viewer/favicons/android-chrome-512x512.png -------------------------------------------------------------------------------- /smol/viewer/static/viewer/favicons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EinAeffchen/Smol/2ee58c4647973818bb3c124f70a92b71e3b7f5c0/smol/viewer/static/viewer/favicons/apple-touch-icon.png -------------------------------------------------------------------------------- /smol/viewer/static/viewer/favicons/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #da532c 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /smol/viewer/static/viewer/favicons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EinAeffchen/Smol/2ee58c4647973818bb3c124f70a92b71e3b7f5c0/smol/viewer/static/viewer/favicons/favicon-16x16.png -------------------------------------------------------------------------------- /smol/viewer/static/viewer/favicons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EinAeffchen/Smol/2ee58c4647973818bb3c124f70a92b71e3b7f5c0/smol/viewer/static/viewer/favicons/favicon-32x32.png -------------------------------------------------------------------------------- /smol/viewer/static/viewer/favicons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EinAeffchen/Smol/2ee58c4647973818bb3c124f70a92b71e3b7f5c0/smol/viewer/static/viewer/favicons/favicon.ico -------------------------------------------------------------------------------- /smol/viewer/static/viewer/favicons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EinAeffchen/Smol/2ee58c4647973818bb3c124f70a92b71e3b7f5c0/smol/viewer/static/viewer/favicons/mstile-150x150.png -------------------------------------------------------------------------------- /smol/viewer/static/viewer/favicons/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.14, written by Peter Selinger 2001-2017 9 | 10 | 12 | 71 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /smol/viewer/static/viewer/favicons/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /smol/viewer/static/viewer/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EinAeffchen/Smol/2ee58c4647973818bb3c124f70a92b71e3b7f5c0/smol/viewer/static/viewer/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /smol/viewer/static/viewer/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EinAeffchen/Smol/2ee58c4647973818bb3c124f70a92b71e3b7f5c0/smol/viewer/static/viewer/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /smol/viewer/static/viewer/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EinAeffchen/Smol/2ee58c4647973818bb3c124f70a92b71e3b7f5c0/smol/viewer/static/viewer/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /smol/viewer/static/viewer/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EinAeffchen/Smol/2ee58c4647973818bb3c124f70a92b71e3b7f5c0/smol/viewer/static/viewer/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /smol/viewer/static/viewer/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EinAeffchen/Smol/2ee58c4647973818bb3c124f70a92b71e3b7f5c0/smol/viewer/static/viewer/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /smol/viewer/static/viewer/js/filter.js: -------------------------------------------------------------------------------- 1 | // variable that keeps all the filter information 2 | 3 | var send_data = {} 4 | 5 | $(document).ready(function () { 6 | 7 | 8 | // reset all parameters on page load 9 | 10 | // resetFilters(); 11 | // bring all the data without any filters 12 | 13 | getAPIData(); 14 | // get all countries from database via 15 | 16 | // sort the data according to price/points 17 | 18 | $('#id_filter_videos').on('change', function () { 19 | send_data['sort_by'] = this.value; 20 | getAPIData(); 21 | }); 22 | 23 | }) 24 | 25 | 26 | /** 27 | Function that resets all the filters 28 | **/ 29 | function resetFilters() { 30 | $("#sort_by").value = "rating"; 31 | if ($("#ajax-url").attr("portal").length > 0) { 32 | var portal = $("#ajax-url").attr("portal") 33 | send_data["portal"] = portal; 34 | } 35 | else if ($("#ajax-url").attr("tag").length > 0) { 36 | var tag = $("#ajax-url").attr("tag") 37 | send_data["tag"] = tag; 38 | } 39 | else if ($("#ajax-url").attr("category").length > 0) { 40 | var category = $("#ajax-url").attr("category") 41 | send_data["category"] = category; 42 | } 43 | else if ($("#ajax-url").attr("search").length > 0) { 44 | var search = $("#ajax-url").attr("search") 45 | send_data["search"] = search; 46 | } 47 | 48 | send_data["sort_by"] = 'rating'; 49 | send_data['format'] = 'json'; 50 | } 51 | 52 | /**. 53 | Utility function to showcase the api data 54 | we got from backend to the table content 55 | **/ 56 | function putTableData(result) { 57 | // creating table row for each result and 58 | 59 | // pushing to the html cntent of table body of listing table 60 | 61 | let row; 62 | if (result["results"].length > 0) { 63 | $("#no_results").hide(); 64 | $("#list_data").show(); 65 | $("#listing").html(""); 66 | $.each(result["results"], function (a, b) { 67 | var title = b.title 68 | title = title.substring(0, 60); 69 | row = '
' + 70 | '' + 71 | '' + title + '' + 73 | '' + 74 | '
' + 75 | '

' + title + '

' + 76 | '
' + 77 | '
' 78 | $("#listing").append(row); 79 | }); 80 | var count = Humanize.intComma(result["count"]) 81 | $("#count").html(count + " Results for:") 82 | } 83 | else { 84 | // if no result found for the given filter, then display no result 85 | 86 | $("#no_results h5").html("No results found"); 87 | $("#list_data").hide(); 88 | $("#no_results").show(); 89 | } 90 | // setting previous and next page url for the given result 91 | 92 | let first_url = result["links"]["first"]; 93 | let prev_url = result["links"]["previous"]; 94 | let current = result["links"]["current"]; 95 | let next_url = result["links"]["next"]; 96 | let last_url = result["links"]["last"]; 97 | let total = result["total_pages"]; 98 | // disabling-enabling button depending on existence of next/prev page. 99 | 100 | if (prev_url === null) { 101 | $("#previous").addClass("disabled"); 102 | $("#previous").prop('disabled', true); 103 | $("#first").addClass("disabled"); 104 | $("#first").prop('disabled', true); 105 | } else { 106 | $("#previous").removeClass("disabled"); 107 | $("#previous").prop('disabled', false); 108 | $("#first").removeClass("disabled"); 109 | $("#first").prop('disabled', false); 110 | } 111 | if (next_url === null) { 112 | $("#next").addClass("disabled"); 113 | $("#next").prop('disabled', true); 114 | $("#last").addClass("disabled"); 115 | $("#last").prop('disabled', true); 116 | } else { 117 | $("#next").removeClass("disabled"); 118 | $("#next").prop('disabled', false); 119 | $("#last").removeClass("disabled"); 120 | $("#last").prop('disabled', false); 121 | } 122 | // setting the url 123 | 124 | $("#first").attr("url", first_url); 125 | $("#previous").attr("url", prev_url); 126 | $("#current").html("Page " + Humanize.intComma(current) + " of " + Humanize.intComma(total)) 127 | $("#next").attr("url", next_url); 128 | $("#last").attr("url", last_url); 129 | // displaying result count 130 | 131 | 132 | $(".fancybox").fancybox({ 133 | openEffect: "none", 134 | closeEffect: "none" 135 | }); 136 | 137 | $(".zoom").hover(function () { 138 | 139 | $(this).addClass('transition'); 140 | }, function () { 141 | 142 | $(this).removeClass('transition'); 143 | }); 144 | } 145 | 146 | function getAPIData() { 147 | let url = $('#ajax-url').attr("url") 148 | $.ajax({ 149 | method: 'GET', 150 | url: url, 151 | data: send_data, 152 | beforeSend: function () { 153 | $("#no_results h5").html("Loading data..."); 154 | }, 155 | success: function (result) { 156 | putTableData(result); 157 | }, 158 | error: function (response) { 159 | $("#no_results h5").html("Something went wrong"); 160 | $("#list_data").hide(); 161 | } 162 | }); 163 | } 164 | 165 | $("#next").click(function () { 166 | // load the next page data and 167 | 168 | // put the result to the table body 169 | 170 | // by making ajax call to next available url 171 | 172 | let url = $(this).attr("url"); 173 | if (!url) 174 | $(this).prop('all', true); 175 | 176 | $(this).prop('all', false); 177 | $.ajax({ 178 | method: 'GET', 179 | url: url, 180 | success: function (result) { 181 | putTableData(result); 182 | }, 183 | error: function (response) { 184 | console.log(response) 185 | } 186 | }); 187 | }) 188 | 189 | $("#previous").click(function () { 190 | // load the previous page data and 191 | 192 | // put the result to the table body 193 | 194 | // by making ajax call to previous available url 195 | 196 | let url = $(this).attr("url"); 197 | if (!url) 198 | $(this).prop('all', true); 199 | 200 | $(this).prop('all', false); 201 | $.ajax({ 202 | method: 'GET', 203 | url: url, 204 | success: function (result) { 205 | putTableData(result); 206 | }, 207 | error: function (response) { 208 | console.log(response) 209 | } 210 | }); 211 | }) 212 | 213 | $("#first").click(function () { 214 | // load the next page data and 215 | 216 | // put the result to the table body 217 | 218 | // by making ajax call to next available url 219 | 220 | let url = $(this).attr("url"); 221 | if (!url) 222 | $(this).prop('all', true); 223 | 224 | $(this).prop('all', false); 225 | $.ajax({ 226 | method: 'GET', 227 | url: url, 228 | success: function (result) { 229 | putTableData(result); 230 | }, 231 | error: function (response) { 232 | console.log(response) 233 | } 234 | }); 235 | }) 236 | 237 | $("#last").click(function () { 238 | // load the next page data and 239 | 240 | // put the result to the table body 241 | 242 | // by making ajax call to next available url 243 | 244 | let url = $(this).attr("url"); 245 | if (!url) 246 | $(this).prop('all', true); 247 | 248 | $(this).prop('all', false); 249 | $.ajax({ 250 | method: 'GET', 251 | url: url, 252 | success: function (result) { 253 | putTableData(result); 254 | }, 255 | error: function (response) { 256 | console.log(response) 257 | } 258 | }); 259 | }) -------------------------------------------------------------------------------- /smol/viewer/static/viewer/js/humanize.min.js: -------------------------------------------------------------------------------- 1 | /* humanize.min.js - v1.8.2 */ 2 | "use strict";var _typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(n){return typeof n}:function(n){return n&&"function"==typeof Symbol&&n.constructor===Symbol?"symbol":typeof n};!function(n,e){"object"===("undefined"==typeof exports?"undefined":_typeof(exports))?module.exports=e():"function"==typeof define&&define.amd?define([],function(){return n.Humanize=e()}):n.Humanize=e()}(this,function(){var n=[{name:"second",value:1e3},{name:"minute",value:6e4},{name:"hour",value:36e5},{name:"day",value:864e5},{name:"week",value:6048e5}],e={P:Math.pow(2,50),T:Math.pow(2,40),G:Math.pow(2,30),M:Math.pow(2,20)},t=function(n){return"undefined"!=typeof n&&null!==n},r=function(n){return n!==n},i=function(n){return isFinite(n)&&!r(parseFloat(n))},o=function(n){var e=Object.prototype.toString.call(n);return"[object Array]"===e},a={intword:function(n,e){var t=arguments.length<=2||void 0===arguments[2]?2:arguments[2];return a.compactInteger(n,t)},compactInteger:function(n){var e=arguments.length<=1||void 0===arguments[1]?0:arguments[1];e=Math.max(e,0);var t=parseInt(n,10),r=0>t?"-":"",i=Math.abs(t),o=String(i),a=o.length,u=[13,10,7,4],l=["T","B","M","k"];if(1e3>i)return""+r+o;if(a>u[0]+3)return t.toExponential(e).replace("e+","x10^");for(var f=void 0,c=0;c=v){f=v;break}}var s=a-f+1,d=o.split(""),p=d.slice(0,s),h=d.slice(s,s+e+1),g=p.join(""),m=h.join("");m.length=i)return a.formatNumber(n/i,t,"")+" "+r+"B"}return n>=1024?a.formatNumber(n/1024,0)+" KB":a.formatNumber(n,0)+a.pluralize(n," byte")},filesize:function(){return a.fileSize.apply(a,arguments)},formatNumber:function(n){var e=arguments.length<=1||void 0===arguments[1]?0:arguments[1],t=arguments.length<=2||void 0===arguments[2]?",":arguments[2],r=arguments.length<=3||void 0===arguments[3]?".":arguments[3],i=function(n,e,t){return t?n.substr(0,t)+e:""},o=function(n,e,t){return n.substr(t).replace(/(\d{3})(?=\d)/g,"$1"+e)},u=function(n,e,t){return t?e+a.toFixed(Math.abs(n),t).split(".")[1]:""},l=a.normalizePrecision(e),f=0>n&&"-"||"",c=String(parseInt(a.toFixed(Math.abs(n||0),l),10)),v=c.length>3?c.length%3:0;return f+i(c,t,v)+o(c,t,v)+u(n,r,l)},toFixed:function(n,e){e=t(e)?e:a.normalizePrecision(e,0);var r=Math.pow(10,e);return(Math.round(n*r)/r).toFixed(e)},normalizePrecision:function(n,e){return n=Math.round(Math.abs(n)),r(n)?e:n},ordinal:function(n){var e=parseInt(n,10);if(0===e)return n;var t=e%100;if([11,12,13].indexOf(t)>=0)return e+"th";var r=e%10,i=void 0;switch(r){case 1:i="st";break;case 2:i="nd";break;case 3:i="rd";break;default:i="th"}return""+e+i},times:function(n){var e=arguments.length<=1||void 0===arguments[1]?{}:arguments[1];if(i(n)&&n>=0){var r=parseFloat(n),o=["never","once","twice"];if(t(e[r]))return String(e[r]);var a=t(o[r])&&o[r].toString();return a||r.toString()+" times"}return null},pluralize:function(n,e,r){return t(n)&&t(e)?(r=t(r)?r:e+"s",1===parseInt(n,10)?e:r):null},truncate:function(n){var e=arguments.length<=1||void 0===arguments[1]?100:arguments[1],t=arguments.length<=2||void 0===arguments[2]?"...":arguments[2];return n.length>e?n.substring(0,e-t.length)+t:n},truncateWords:function(n,e){for(var r=n.split(" "),i="",o=0;e>o;)t(r[o])&&(i+=r[o]+" "),o++;return r.length>e?i+"...":null},truncatewords:function(){return a.truncateWords.apply(a,arguments)},boundedNumber:function(n){var e=arguments.length<=1||void 0===arguments[1]?100:arguments[1],t=arguments.length<=2||void 0===arguments[2]?"+":arguments[2],r=void 0;return i(n)&&i(e)&&n>e&&(r=e+t),(r||n).toString()},truncatenumber:function(){return a.boundedNumber.apply(a,arguments)},oxford:function(n,e,r){var i=n.length,o=void 0;if(2>i)return String(n);if(2===i)return n.join(" and ");if(t(e)&&i>e){var u=i-e;o=e,r=t(r)?r:", and "+u+" "+a.pluralize(u,"other")}else o=-1,r=", and "+n[i-1];return n.slice(0,o).join(", ")+r},dictionary:function(n){var e=arguments.length<=1||void 0===arguments[1]?" is ":arguments[1],r=arguments.length<=2||void 0===arguments[2]?", ":arguments[2],i="";if(t(n)&&"object"===("undefined"==typeof n?"undefined":_typeof(n))&&!o(n)){var a=[];for(var u in n)if(n.hasOwnProperty(u)){var l=n[u];a.push(""+u+e+l)}return a.join(r)}return i},frequency:function(n,e){if(!o(n))return null;var t=n.length,r=a.times(t);return 0===t?r+" "+e:e+" "+r},pace:function(e,t){var r=arguments.length<=2||void 0===arguments[2]?"time":arguments[2];if(0===e||0===t)return"No "+a.pluralize(0,r);for(var i="Approximately",o=void 0,u=void 0,l=e/t,f=0;f1){o=c.name;break}}o||(i="Less than",u=1,o=n[n.length-1].name);var v=Math.round(u);return r=a.pluralize(v,r),i+" "+v+" "+r+" per "+o},nl2br:function(n){var e=arguments.length<=1||void 0===arguments[1]?"
":arguments[1];return n.replace(/\n/g,e)},br2nl:function(n){var e=arguments.length<=1||void 0===arguments[1]?"\r\n":arguments[1];return n.replace(/\/g,e)},capitalize:function(n){var e=arguments.length<=1||void 0===arguments[1]?!1:arguments[1];return""+n.charAt(0).toUpperCase()+(e?n.slice(1).toLowerCase():n.slice(1))},capitalizeAll:function(n){return n.replace(/(?:^|\s)\S/g,function(n){return n.toUpperCase()})},titleCase:function(n){var e=/\b(a|an|and|at|but|by|de|en|for|if|in|of|on|or|the|to|via|vs?\.?)\b/i,t=/\S+[A-Z]+\S*/,r=/\s+/,i=/-/,o=void 0;return(o=function(n){for(var u=arguments.length<=1||void 0===arguments[1]?!1:arguments[1],l=arguments.length<=2||void 0===arguments[2]?!0:arguments[2],f=[],c=n.split(u?i:r),v=0;v response.json()) 8 | .then(function (response) { 9 | console.log(response); 10 | const info_box = document.querySelector("#info-box"); 11 | let count_block = new ImportedDocument( 12 | response["file"], 13 | response["thumbnail"], 14 | response["id"] 15 | ); 16 | info_box.innerHTML = count_block.innerHTML; 17 | }); 18 | } 19 | 20 | class DocumentCount { 21 | constructor(count) { 22 | this.innerHTML = `
23 | Found ${count} videos. Start processing... 24 |
`; 25 | } 26 | } 27 | 28 | class ImportedDocument { 29 | constructor(filename, preview, id) { 30 | this.innerHTML = `
Imported ${filename}.
`; 31 | } 32 | } 33 | 34 | function get_new_video() { 35 | let load_icon = document.getElementById("load-icon"); 36 | load_icon.style.display = "block"; 37 | fetch("/get-new-videos/") 38 | .then((response) => console.log(response) || response.json()) 39 | .then(function (response) { 40 | const info_box = document.querySelector("#info-box"); 41 | if (response["paths"]) { 42 | let count_block = new DocumentCount(response["count"]); 43 | info_box.innerHTML = count_block.innerHTML; 44 | for (video of response["paths"]) { 45 | load_video(video); 46 | } 47 | load_icon.style.display = "none"; 48 | } 49 | }); 50 | } 51 | -------------------------------------------------------------------------------- /smol/viewer/static/viewer/js/video_play.js: -------------------------------------------------------------------------------- 1 | function startPreview(video) { 2 | video.children[0].setAttribute( 3 | "src", 4 | video.children[0].getAttribute("src_tmp") 5 | ); 6 | video.muted = true; 7 | video.currentTime = 5; 8 | video.load(); 9 | video.play(); 10 | } 11 | 12 | function stopPreview(video) { 13 | video.pause(); 14 | } 15 | 16 | window.onload = function () { 17 | if (document.URL.includes("/video/")) { 18 | setup_label_tracking(); 19 | } 20 | for (video of document.getElementsByClassName("video-preview")) { 21 | video.addEventListener("mouseenter", (event) => startPreview(event.target)); 22 | video.addEventListener("mouseleave", (event) => stopPreview(event.target)); 23 | } 24 | }; 25 | 26 | function label_exists(label, selector) { 27 | for (existing_label of selector.options) { 28 | if (label === existing_label.value) { 29 | return true; 30 | } 31 | } 32 | return false; 33 | } 34 | 35 | function setup_label_tracking() { 36 | document.getElementById("id_labels").onchange = function (e) { 37 | var chosen_labels = Array.from(e.target.options).filter(function (option) { 38 | return option.selected; 39 | }); 40 | console.log(chosen_labels); 41 | xhr = new XMLHttpRequest(); 42 | xhr.open("POST", "/addVideoLabel/", true); 43 | xhr.setRequestHeader("Content-Type", "application/json"); 44 | var labels = chosen_labels.map(function (option) { 45 | return option.value; 46 | }); 47 | var post_data = { labels: labels }; 48 | post_data["video_id"] = document.URL.split("/")[4]; 49 | console.log(post_data); 50 | console.log(JSON.stringify(post_data)); 51 | for (label in chosen_labels) { 52 | xhr.send(JSON.stringify(post_data)); 53 | } 54 | }; 55 | } 56 | 57 | function addfav(button) { 58 | const Http = new XMLHttpRequest(); 59 | const url = "/fav/" + document.URL.split("/")[4] + "/"; 60 | Http.open("GET", url); 61 | Http.send(); 62 | console.log(button); 63 | button.setAttribute("onClick", "remfav(this)"); 64 | button.setAttribute("id", "rem-fav"); 65 | button.children[0].setAttribute("class", "fa fa-heart"); 66 | } 67 | async function analyze() { 68 | let loader = document.getElementById("load-icon"); 69 | loader.style.display = "none"; 70 | loader.innerHTML = 71 | ' Running face recognition. Depening on your library size, this might take a while'; 72 | loader.style.display = "block"; 73 | console.log(loader); 74 | const response = await fetch("/analyze/", { 75 | method: "POST", 76 | body: JSON.stringify({ video_id: document.URL.split("/")[4] }), 77 | }); 78 | console.log(response); 79 | loader.innerHTML = ' Done'; 80 | location.reload(); 81 | } 82 | // TODO add new element instead of reusing loader 83 | async function deleteEncoding(button) { 84 | let loader = document.getElementById("load-icon"); 85 | loader.style.display = "none"; 86 | loader.innerHTML = 87 | ' Deleting recognition data...'; 88 | loader.style.display = "block"; 89 | const response = await fetch("/delete-encoding/", { 90 | method: "POST", 91 | body: JSON.stringify({ video_id: document.URL.split("/")[4] }), 92 | }); 93 | loader.innerHTML = 94 | ' Deleted Recognition data!'; 95 | } 96 | function remfav(button) { 97 | const Http = new XMLHttpRequest(); 98 | const url = "/remfav/" + document.URL.split("/")[4] + "/"; 99 | Http.open("GET", url); 100 | Http.send(); 101 | console.log(button); 102 | button.setAttribute("onClick", "addfav(this)"); 103 | button.setAttribute("id", "add-fav"); 104 | button.children[0].setAttribute("class", "fa fa-heart-o"); 105 | } 106 | function remvid(button) { 107 | const xhr = new XMLHttpRequest(); 108 | var video_id = document.URL.split("/")[4]; 109 | xhr.open("POST", "/remvid/", true); 110 | xhr.setRequestHeader("Content-Type", "application/json"); 111 | var post_data = { video_id: video_id }; 112 | xhr.send(JSON.stringify(post_data)); 113 | window.location.href = "/"; 114 | } 115 | async function remmeta(button) { 116 | const response = await fetch("/remmeta/", { 117 | method: "POST", 118 | body: JSON.stringify({ video_id: document.URL.split("/")[4] }), 119 | }); 120 | const output = await response; 121 | console.log(output); 122 | window.location.href = "/"; 123 | } 124 | -------------------------------------------------------------------------------- /smol/viewer/static/viewer/less/animated.less: -------------------------------------------------------------------------------- 1 | // Animated Icons 2 | // -------------------------- 3 | 4 | .@{fa-css-prefix}-spin { 5 | -webkit-animation: fa-spin 2s infinite linear; 6 | animation: fa-spin 2s infinite linear; 7 | } 8 | 9 | .@{fa-css-prefix}-pulse { 10 | -webkit-animation: fa-spin 1s infinite steps(8); 11 | animation: fa-spin 1s infinite steps(8); 12 | } 13 | 14 | @-webkit-keyframes fa-spin { 15 | 0% { 16 | -webkit-transform: rotate(0deg); 17 | transform: rotate(0deg); 18 | } 19 | 100% { 20 | -webkit-transform: rotate(359deg); 21 | transform: rotate(359deg); 22 | } 23 | } 24 | 25 | @keyframes fa-spin { 26 | 0% { 27 | -webkit-transform: rotate(0deg); 28 | transform: rotate(0deg); 29 | } 30 | 100% { 31 | -webkit-transform: rotate(359deg); 32 | transform: rotate(359deg); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /smol/viewer/static/viewer/less/bordered-pulled.less: -------------------------------------------------------------------------------- 1 | // Bordered & Pulled 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix}-border { 5 | padding: .2em .25em .15em; 6 | border: solid .08em @fa-border-color; 7 | border-radius: .1em; 8 | } 9 | 10 | .@{fa-css-prefix}-pull-left { float: left; } 11 | .@{fa-css-prefix}-pull-right { float: right; } 12 | 13 | .@{fa-css-prefix} { 14 | &.@{fa-css-prefix}-pull-left { margin-right: .3em; } 15 | &.@{fa-css-prefix}-pull-right { margin-left: .3em; } 16 | } 17 | 18 | /* Deprecated as of 4.4.0 */ 19 | .pull-right { float: right; } 20 | .pull-left { float: left; } 21 | 22 | .@{fa-css-prefix} { 23 | &.pull-left { margin-right: .3em; } 24 | &.pull-right { margin-left: .3em; } 25 | } 26 | -------------------------------------------------------------------------------- /smol/viewer/static/viewer/less/core.less: -------------------------------------------------------------------------------- 1 | // Base Class Definition 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix} { 5 | display: inline-block; 6 | font: normal normal normal @fa-font-size-base/@fa-line-height-base FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | 12 | } 13 | -------------------------------------------------------------------------------- /smol/viewer/static/viewer/less/fixed-width.less: -------------------------------------------------------------------------------- 1 | // Fixed Width Icons 2 | // ------------------------- 3 | .@{fa-css-prefix}-fw { 4 | width: (18em / 14); 5 | text-align: center; 6 | } 7 | -------------------------------------------------------------------------------- /smol/viewer/static/viewer/less/font-awesome.less: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome 3 | * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) 4 | */ 5 | 6 | @import "variables.less"; 7 | @import "mixins.less"; 8 | @import "path.less"; 9 | @import "core.less"; 10 | @import "larger.less"; 11 | @import "fixed-width.less"; 12 | @import "list.less"; 13 | @import "bordered-pulled.less"; 14 | @import "animated.less"; 15 | @import "rotated-flipped.less"; 16 | @import "stacked.less"; 17 | @import "icons.less"; 18 | @import "screen-reader.less"; 19 | -------------------------------------------------------------------------------- /smol/viewer/static/viewer/less/larger.less: -------------------------------------------------------------------------------- 1 | // Icon Sizes 2 | // ------------------------- 3 | 4 | /* makes the font 33% larger relative to the icon container */ 5 | .@{fa-css-prefix}-lg { 6 | font-size: (4em / 3); 7 | line-height: (3em / 4); 8 | vertical-align: -15%; 9 | } 10 | .@{fa-css-prefix}-2x { font-size: 2em; } 11 | .@{fa-css-prefix}-3x { font-size: 3em; } 12 | .@{fa-css-prefix}-4x { font-size: 4em; } 13 | .@{fa-css-prefix}-5x { font-size: 5em; } 14 | -------------------------------------------------------------------------------- /smol/viewer/static/viewer/less/list.less: -------------------------------------------------------------------------------- 1 | // List Icons 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix}-ul { 5 | padding-left: 0; 6 | margin-left: @fa-li-width; 7 | list-style-type: none; 8 | > li { position: relative; } 9 | } 10 | .@{fa-css-prefix}-li { 11 | position: absolute; 12 | left: -@fa-li-width; 13 | width: @fa-li-width; 14 | top: (2em / 14); 15 | text-align: center; 16 | &.@{fa-css-prefix}-lg { 17 | left: (-@fa-li-width + (4em / 14)); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /smol/viewer/static/viewer/less/mixins.less: -------------------------------------------------------------------------------- 1 | // Mixins 2 | // -------------------------- 3 | 4 | .fa-icon() { 5 | display: inline-block; 6 | font: normal normal normal @fa-font-size-base/@fa-line-height-base FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | 12 | } 13 | 14 | .fa-icon-rotate(@degrees, @rotation) { 15 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=@{rotation})"; 16 | -webkit-transform: rotate(@degrees); 17 | -ms-transform: rotate(@degrees); 18 | transform: rotate(@degrees); 19 | } 20 | 21 | .fa-icon-flip(@horiz, @vert, @rotation) { 22 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=@{rotation}, mirror=1)"; 23 | -webkit-transform: scale(@horiz, @vert); 24 | -ms-transform: scale(@horiz, @vert); 25 | transform: scale(@horiz, @vert); 26 | } 27 | 28 | 29 | // Only display content to screen readers. A la Bootstrap 4. 30 | // 31 | // See: http://a11yproject.com/posts/how-to-hide-content/ 32 | 33 | .sr-only() { 34 | position: absolute; 35 | width: 1px; 36 | height: 1px; 37 | padding: 0; 38 | margin: -1px; 39 | overflow: hidden; 40 | clip: rect(0,0,0,0); 41 | border: 0; 42 | } 43 | 44 | // Use in conjunction with .sr-only to only display content when it's focused. 45 | // 46 | // Useful for "Skip to main content" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1 47 | // 48 | // Credit: HTML5 Boilerplate 49 | 50 | .sr-only-focusable() { 51 | &:active, 52 | &:focus { 53 | position: static; 54 | width: auto; 55 | height: auto; 56 | margin: 0; 57 | overflow: visible; 58 | clip: auto; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /smol/viewer/static/viewer/less/path.less: -------------------------------------------------------------------------------- 1 | /* FONT PATH 2 | * -------------------------- */ 3 | 4 | @font-face { 5 | font-family: 'FontAwesome'; 6 | src: url('@{fa-font-path}/fontawesome-webfont.eot?v=@{fa-version}'); 7 | src: url('@{fa-font-path}/fontawesome-webfont.eot?#iefix&v=@{fa-version}') format('embedded-opentype'), 8 | url('@{fa-font-path}/fontawesome-webfont.woff2?v=@{fa-version}') format('woff2'), 9 | url('@{fa-font-path}/fontawesome-webfont.woff?v=@{fa-version}') format('woff'), 10 | url('@{fa-font-path}/fontawesome-webfont.ttf?v=@{fa-version}') format('truetype'), 11 | url('@{fa-font-path}/fontawesome-webfont.svg?v=@{fa-version}#fontawesomeregular') format('svg'); 12 | // src: url('@{fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts 13 | font-weight: normal; 14 | font-style: normal; 15 | } 16 | -------------------------------------------------------------------------------- /smol/viewer/static/viewer/less/rotated-flipped.less: -------------------------------------------------------------------------------- 1 | // Rotated & Flipped Icons 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix}-rotate-90 { .fa-icon-rotate(90deg, 1); } 5 | .@{fa-css-prefix}-rotate-180 { .fa-icon-rotate(180deg, 2); } 6 | .@{fa-css-prefix}-rotate-270 { .fa-icon-rotate(270deg, 3); } 7 | 8 | .@{fa-css-prefix}-flip-horizontal { .fa-icon-flip(-1, 1, 0); } 9 | .@{fa-css-prefix}-flip-vertical { .fa-icon-flip(1, -1, 2); } 10 | 11 | // Hook for IE8-9 12 | // ------------------------- 13 | 14 | :root .@{fa-css-prefix}-rotate-90, 15 | :root .@{fa-css-prefix}-rotate-180, 16 | :root .@{fa-css-prefix}-rotate-270, 17 | :root .@{fa-css-prefix}-flip-horizontal, 18 | :root .@{fa-css-prefix}-flip-vertical { 19 | filter: none; 20 | } 21 | -------------------------------------------------------------------------------- /smol/viewer/static/viewer/less/screen-reader.less: -------------------------------------------------------------------------------- 1 | // Screen Readers 2 | // ------------------------- 3 | 4 | .sr-only { .sr-only(); } 5 | .sr-only-focusable { .sr-only-focusable(); } 6 | -------------------------------------------------------------------------------- /smol/viewer/static/viewer/less/stacked.less: -------------------------------------------------------------------------------- 1 | // Stacked Icons 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix}-stack { 5 | position: relative; 6 | display: inline-block; 7 | width: 2em; 8 | height: 2em; 9 | line-height: 2em; 10 | vertical-align: middle; 11 | } 12 | .@{fa-css-prefix}-stack-1x, .@{fa-css-prefix}-stack-2x { 13 | position: absolute; 14 | left: 0; 15 | width: 100%; 16 | text-align: center; 17 | } 18 | .@{fa-css-prefix}-stack-1x { line-height: inherit; } 19 | .@{fa-css-prefix}-stack-2x { font-size: 2em; } 20 | .@{fa-css-prefix}-inverse { color: @fa-inverse; } 21 | -------------------------------------------------------------------------------- /smol/viewer/static/viewer/less/variables.less: -------------------------------------------------------------------------------- 1 | // Variables 2 | // -------------------------- 3 | 4 | @fa-font-path: "../fonts"; 5 | @fa-font-size-base: 14px; 6 | @fa-line-height-base: 1; 7 | //@fa-font-path: "//netdna.bootstrapcdn.com/font-awesome/4.7.0/fonts"; // for referencing Bootstrap CDN font files directly 8 | @fa-css-prefix: fa; 9 | @fa-version: "4.7.0"; 10 | @fa-border-color: #eee; 11 | @fa-inverse: #fff; 12 | @fa-li-width: (30em / 14); 13 | 14 | @fa-var-500px: "\f26e"; 15 | @fa-var-address-book: "\f2b9"; 16 | @fa-var-address-book-o: "\f2ba"; 17 | @fa-var-address-card: "\f2bb"; 18 | @fa-var-address-card-o: "\f2bc"; 19 | @fa-var-adjust: "\f042"; 20 | @fa-var-adn: "\f170"; 21 | @fa-var-align-center: "\f037"; 22 | @fa-var-align-justify: "\f039"; 23 | @fa-var-align-left: "\f036"; 24 | @fa-var-align-right: "\f038"; 25 | @fa-var-amazon: "\f270"; 26 | @fa-var-ambulance: "\f0f9"; 27 | @fa-var-american-sign-language-interpreting: "\f2a3"; 28 | @fa-var-anchor: "\f13d"; 29 | @fa-var-android: "\f17b"; 30 | @fa-var-angellist: "\f209"; 31 | @fa-var-angle-double-down: "\f103"; 32 | @fa-var-angle-double-left: "\f100"; 33 | @fa-var-angle-double-right: "\f101"; 34 | @fa-var-angle-double-up: "\f102"; 35 | @fa-var-angle-down: "\f107"; 36 | @fa-var-angle-left: "\f104"; 37 | @fa-var-angle-right: "\f105"; 38 | @fa-var-angle-up: "\f106"; 39 | @fa-var-apple: "\f179"; 40 | @fa-var-archive: "\f187"; 41 | @fa-var-area-chart: "\f1fe"; 42 | @fa-var-arrow-circle-down: "\f0ab"; 43 | @fa-var-arrow-circle-left: "\f0a8"; 44 | @fa-var-arrow-circle-o-down: "\f01a"; 45 | @fa-var-arrow-circle-o-left: "\f190"; 46 | @fa-var-arrow-circle-o-right: "\f18e"; 47 | @fa-var-arrow-circle-o-up: "\f01b"; 48 | @fa-var-arrow-circle-right: "\f0a9"; 49 | @fa-var-arrow-circle-up: "\f0aa"; 50 | @fa-var-arrow-down: "\f063"; 51 | @fa-var-arrow-left: "\f060"; 52 | @fa-var-arrow-right: "\f061"; 53 | @fa-var-arrow-up: "\f062"; 54 | @fa-var-arrows: "\f047"; 55 | @fa-var-arrows-alt: "\f0b2"; 56 | @fa-var-arrows-h: "\f07e"; 57 | @fa-var-arrows-v: "\f07d"; 58 | @fa-var-asl-interpreting: "\f2a3"; 59 | @fa-var-assistive-listening-systems: "\f2a2"; 60 | @fa-var-asterisk: "\f069"; 61 | @fa-var-at: "\f1fa"; 62 | @fa-var-audio-description: "\f29e"; 63 | @fa-var-automobile: "\f1b9"; 64 | @fa-var-backward: "\f04a"; 65 | @fa-var-balance-scale: "\f24e"; 66 | @fa-var-ban: "\f05e"; 67 | @fa-var-bandcamp: "\f2d5"; 68 | @fa-var-bank: "\f19c"; 69 | @fa-var-bar-chart: "\f080"; 70 | @fa-var-bar-chart-o: "\f080"; 71 | @fa-var-barcode: "\f02a"; 72 | @fa-var-bars: "\f0c9"; 73 | @fa-var-bath: "\f2cd"; 74 | @fa-var-bathtub: "\f2cd"; 75 | @fa-var-battery: "\f240"; 76 | @fa-var-battery-0: "\f244"; 77 | @fa-var-battery-1: "\f243"; 78 | @fa-var-battery-2: "\f242"; 79 | @fa-var-battery-3: "\f241"; 80 | @fa-var-battery-4: "\f240"; 81 | @fa-var-battery-empty: "\f244"; 82 | @fa-var-battery-full: "\f240"; 83 | @fa-var-battery-half: "\f242"; 84 | @fa-var-battery-quarter: "\f243"; 85 | @fa-var-battery-three-quarters: "\f241"; 86 | @fa-var-bed: "\f236"; 87 | @fa-var-beer: "\f0fc"; 88 | @fa-var-behance: "\f1b4"; 89 | @fa-var-behance-square: "\f1b5"; 90 | @fa-var-bell: "\f0f3"; 91 | @fa-var-bell-o: "\f0a2"; 92 | @fa-var-bell-slash: "\f1f6"; 93 | @fa-var-bell-slash-o: "\f1f7"; 94 | @fa-var-bicycle: "\f206"; 95 | @fa-var-binoculars: "\f1e5"; 96 | @fa-var-birthday-cake: "\f1fd"; 97 | @fa-var-bitbucket: "\f171"; 98 | @fa-var-bitbucket-square: "\f172"; 99 | @fa-var-bitcoin: "\f15a"; 100 | @fa-var-black-tie: "\f27e"; 101 | @fa-var-blind: "\f29d"; 102 | @fa-var-bluetooth: "\f293"; 103 | @fa-var-bluetooth-b: "\f294"; 104 | @fa-var-bold: "\f032"; 105 | @fa-var-bolt: "\f0e7"; 106 | @fa-var-bomb: "\f1e2"; 107 | @fa-var-book: "\f02d"; 108 | @fa-var-bookmark: "\f02e"; 109 | @fa-var-bookmark-o: "\f097"; 110 | @fa-var-braille: "\f2a1"; 111 | @fa-var-briefcase: "\f0b1"; 112 | @fa-var-btc: "\f15a"; 113 | @fa-var-bug: "\f188"; 114 | @fa-var-building: "\f1ad"; 115 | @fa-var-building-o: "\f0f7"; 116 | @fa-var-bullhorn: "\f0a1"; 117 | @fa-var-bullseye: "\f140"; 118 | @fa-var-bus: "\f207"; 119 | @fa-var-buysellads: "\f20d"; 120 | @fa-var-cab: "\f1ba"; 121 | @fa-var-calculator: "\f1ec"; 122 | @fa-var-calendar: "\f073"; 123 | @fa-var-calendar-check-o: "\f274"; 124 | @fa-var-calendar-minus-o: "\f272"; 125 | @fa-var-calendar-o: "\f133"; 126 | @fa-var-calendar-plus-o: "\f271"; 127 | @fa-var-calendar-times-o: "\f273"; 128 | @fa-var-camera: "\f030"; 129 | @fa-var-camera-retro: "\f083"; 130 | @fa-var-car: "\f1b9"; 131 | @fa-var-caret-down: "\f0d7"; 132 | @fa-var-caret-left: "\f0d9"; 133 | @fa-var-caret-right: "\f0da"; 134 | @fa-var-caret-square-o-down: "\f150"; 135 | @fa-var-caret-square-o-left: "\f191"; 136 | @fa-var-caret-square-o-right: "\f152"; 137 | @fa-var-caret-square-o-up: "\f151"; 138 | @fa-var-caret-up: "\f0d8"; 139 | @fa-var-cart-arrow-down: "\f218"; 140 | @fa-var-cart-plus: "\f217"; 141 | @fa-var-cc: "\f20a"; 142 | @fa-var-cc-amex: "\f1f3"; 143 | @fa-var-cc-diners-club: "\f24c"; 144 | @fa-var-cc-discover: "\f1f2"; 145 | @fa-var-cc-jcb: "\f24b"; 146 | @fa-var-cc-mastercard: "\f1f1"; 147 | @fa-var-cc-paypal: "\f1f4"; 148 | @fa-var-cc-stripe: "\f1f5"; 149 | @fa-var-cc-visa: "\f1f0"; 150 | @fa-var-certificate: "\f0a3"; 151 | @fa-var-chain: "\f0c1"; 152 | @fa-var-chain-broken: "\f127"; 153 | @fa-var-check: "\f00c"; 154 | @fa-var-check-circle: "\f058"; 155 | @fa-var-check-circle-o: "\f05d"; 156 | @fa-var-check-square: "\f14a"; 157 | @fa-var-check-square-o: "\f046"; 158 | @fa-var-chevron-circle-down: "\f13a"; 159 | @fa-var-chevron-circle-left: "\f137"; 160 | @fa-var-chevron-circle-right: "\f138"; 161 | @fa-var-chevron-circle-up: "\f139"; 162 | @fa-var-chevron-down: "\f078"; 163 | @fa-var-chevron-left: "\f053"; 164 | @fa-var-chevron-right: "\f054"; 165 | @fa-var-chevron-up: "\f077"; 166 | @fa-var-child: "\f1ae"; 167 | @fa-var-chrome: "\f268"; 168 | @fa-var-circle: "\f111"; 169 | @fa-var-circle-o: "\f10c"; 170 | @fa-var-circle-o-notch: "\f1ce"; 171 | @fa-var-circle-thin: "\f1db"; 172 | @fa-var-clipboard: "\f0ea"; 173 | @fa-var-clock-o: "\f017"; 174 | @fa-var-clone: "\f24d"; 175 | @fa-var-close: "\f00d"; 176 | @fa-var-cloud: "\f0c2"; 177 | @fa-var-cloud-download: "\f0ed"; 178 | @fa-var-cloud-upload: "\f0ee"; 179 | @fa-var-cny: "\f157"; 180 | @fa-var-code: "\f121"; 181 | @fa-var-code-fork: "\f126"; 182 | @fa-var-codepen: "\f1cb"; 183 | @fa-var-codiepie: "\f284"; 184 | @fa-var-coffee: "\f0f4"; 185 | @fa-var-cog: "\f013"; 186 | @fa-var-cogs: "\f085"; 187 | @fa-var-columns: "\f0db"; 188 | @fa-var-comment: "\f075"; 189 | @fa-var-comment-o: "\f0e5"; 190 | @fa-var-commenting: "\f27a"; 191 | @fa-var-commenting-o: "\f27b"; 192 | @fa-var-comments: "\f086"; 193 | @fa-var-comments-o: "\f0e6"; 194 | @fa-var-compass: "\f14e"; 195 | @fa-var-compress: "\f066"; 196 | @fa-var-connectdevelop: "\f20e"; 197 | @fa-var-contao: "\f26d"; 198 | @fa-var-copy: "\f0c5"; 199 | @fa-var-copyright: "\f1f9"; 200 | @fa-var-creative-commons: "\f25e"; 201 | @fa-var-credit-card: "\f09d"; 202 | @fa-var-credit-card-alt: "\f283"; 203 | @fa-var-crop: "\f125"; 204 | @fa-var-crosshairs: "\f05b"; 205 | @fa-var-css3: "\f13c"; 206 | @fa-var-cube: "\f1b2"; 207 | @fa-var-cubes: "\f1b3"; 208 | @fa-var-cut: "\f0c4"; 209 | @fa-var-cutlery: "\f0f5"; 210 | @fa-var-dashboard: "\f0e4"; 211 | @fa-var-dashcube: "\f210"; 212 | @fa-var-database: "\f1c0"; 213 | @fa-var-deaf: "\f2a4"; 214 | @fa-var-deafness: "\f2a4"; 215 | @fa-var-dedent: "\f03b"; 216 | @fa-var-delicious: "\f1a5"; 217 | @fa-var-desktop: "\f108"; 218 | @fa-var-deviantart: "\f1bd"; 219 | @fa-var-diamond: "\f219"; 220 | @fa-var-digg: "\f1a6"; 221 | @fa-var-dollar: "\f155"; 222 | @fa-var-dot-circle-o: "\f192"; 223 | @fa-var-download: "\f019"; 224 | @fa-var-dribbble: "\f17d"; 225 | @fa-var-drivers-license: "\f2c2"; 226 | @fa-var-drivers-license-o: "\f2c3"; 227 | @fa-var-dropbox: "\f16b"; 228 | @fa-var-drupal: "\f1a9"; 229 | @fa-var-edge: "\f282"; 230 | @fa-var-edit: "\f044"; 231 | @fa-var-eercast: "\f2da"; 232 | @fa-var-eject: "\f052"; 233 | @fa-var-ellipsis-h: "\f141"; 234 | @fa-var-ellipsis-v: "\f142"; 235 | @fa-var-empire: "\f1d1"; 236 | @fa-var-envelope: "\f0e0"; 237 | @fa-var-envelope-o: "\f003"; 238 | @fa-var-envelope-open: "\f2b6"; 239 | @fa-var-envelope-open-o: "\f2b7"; 240 | @fa-var-envelope-square: "\f199"; 241 | @fa-var-envira: "\f299"; 242 | @fa-var-eraser: "\f12d"; 243 | @fa-var-etsy: "\f2d7"; 244 | @fa-var-eur: "\f153"; 245 | @fa-var-euro: "\f153"; 246 | @fa-var-exchange: "\f0ec"; 247 | @fa-var-exclamation: "\f12a"; 248 | @fa-var-exclamation-circle: "\f06a"; 249 | @fa-var-exclamation-triangle: "\f071"; 250 | @fa-var-expand: "\f065"; 251 | @fa-var-expeditedssl: "\f23e"; 252 | @fa-var-external-link: "\f08e"; 253 | @fa-var-external-link-square: "\f14c"; 254 | @fa-var-eye: "\f06e"; 255 | @fa-var-eye-slash: "\f070"; 256 | @fa-var-eyedropper: "\f1fb"; 257 | @fa-var-fa: "\f2b4"; 258 | @fa-var-facebook: "\f09a"; 259 | @fa-var-facebook-f: "\f09a"; 260 | @fa-var-facebook-official: "\f230"; 261 | @fa-var-facebook-square: "\f082"; 262 | @fa-var-fast-backward: "\f049"; 263 | @fa-var-fast-forward: "\f050"; 264 | @fa-var-fax: "\f1ac"; 265 | @fa-var-feed: "\f09e"; 266 | @fa-var-female: "\f182"; 267 | @fa-var-fighter-jet: "\f0fb"; 268 | @fa-var-file: "\f15b"; 269 | @fa-var-file-archive-o: "\f1c6"; 270 | @fa-var-file-audio-o: "\f1c7"; 271 | @fa-var-file-code-o: "\f1c9"; 272 | @fa-var-file-excel-o: "\f1c3"; 273 | @fa-var-file-image-o: "\f1c5"; 274 | @fa-var-file-movie-o: "\f1c8"; 275 | @fa-var-file-o: "\f016"; 276 | @fa-var-file-pdf-o: "\f1c1"; 277 | @fa-var-file-photo-o: "\f1c5"; 278 | @fa-var-file-picture-o: "\f1c5"; 279 | @fa-var-file-powerpoint-o: "\f1c4"; 280 | @fa-var-file-sound-o: "\f1c7"; 281 | @fa-var-file-text: "\f15c"; 282 | @fa-var-file-text-o: "\f0f6"; 283 | @fa-var-file-video-o: "\f1c8"; 284 | @fa-var-file-word-o: "\f1c2"; 285 | @fa-var-file-zip-o: "\f1c6"; 286 | @fa-var-files-o: "\f0c5"; 287 | @fa-var-film: "\f008"; 288 | @fa-var-filter: "\f0b0"; 289 | @fa-var-fire: "\f06d"; 290 | @fa-var-fire-extinguisher: "\f134"; 291 | @fa-var-firefox: "\f269"; 292 | @fa-var-first-order: "\f2b0"; 293 | @fa-var-flag: "\f024"; 294 | @fa-var-flag-checkered: "\f11e"; 295 | @fa-var-flag-o: "\f11d"; 296 | @fa-var-flash: "\f0e7"; 297 | @fa-var-flask: "\f0c3"; 298 | @fa-var-flickr: "\f16e"; 299 | @fa-var-floppy-o: "\f0c7"; 300 | @fa-var-folder: "\f07b"; 301 | @fa-var-folder-o: "\f114"; 302 | @fa-var-folder-open: "\f07c"; 303 | @fa-var-folder-open-o: "\f115"; 304 | @fa-var-font: "\f031"; 305 | @fa-var-font-awesome: "\f2b4"; 306 | @fa-var-fonticons: "\f280"; 307 | @fa-var-fort-awesome: "\f286"; 308 | @fa-var-forumbee: "\f211"; 309 | @fa-var-forward: "\f04e"; 310 | @fa-var-foursquare: "\f180"; 311 | @fa-var-free-code-camp: "\f2c5"; 312 | @fa-var-frown-o: "\f119"; 313 | @fa-var-futbol-o: "\f1e3"; 314 | @fa-var-gamepad: "\f11b"; 315 | @fa-var-gavel: "\f0e3"; 316 | @fa-var-gbp: "\f154"; 317 | @fa-var-ge: "\f1d1"; 318 | @fa-var-gear: "\f013"; 319 | @fa-var-gears: "\f085"; 320 | @fa-var-genderless: "\f22d"; 321 | @fa-var-get-pocket: "\f265"; 322 | @fa-var-gg: "\f260"; 323 | @fa-var-gg-circle: "\f261"; 324 | @fa-var-gift: "\f06b"; 325 | @fa-var-git: "\f1d3"; 326 | @fa-var-git-square: "\f1d2"; 327 | @fa-var-github: "\f09b"; 328 | @fa-var-github-alt: "\f113"; 329 | @fa-var-github-square: "\f092"; 330 | @fa-var-gitlab: "\f296"; 331 | @fa-var-gittip: "\f184"; 332 | @fa-var-glass: "\f000"; 333 | @fa-var-glide: "\f2a5"; 334 | @fa-var-glide-g: "\f2a6"; 335 | @fa-var-globe: "\f0ac"; 336 | @fa-var-google: "\f1a0"; 337 | @fa-var-google-plus: "\f0d5"; 338 | @fa-var-google-plus-circle: "\f2b3"; 339 | @fa-var-google-plus-official: "\f2b3"; 340 | @fa-var-google-plus-square: "\f0d4"; 341 | @fa-var-google-wallet: "\f1ee"; 342 | @fa-var-graduation-cap: "\f19d"; 343 | @fa-var-gratipay: "\f184"; 344 | @fa-var-grav: "\f2d6"; 345 | @fa-var-group: "\f0c0"; 346 | @fa-var-h-square: "\f0fd"; 347 | @fa-var-hacker-news: "\f1d4"; 348 | @fa-var-hand-grab-o: "\f255"; 349 | @fa-var-hand-lizard-o: "\f258"; 350 | @fa-var-hand-o-down: "\f0a7"; 351 | @fa-var-hand-o-left: "\f0a5"; 352 | @fa-var-hand-o-right: "\f0a4"; 353 | @fa-var-hand-o-up: "\f0a6"; 354 | @fa-var-hand-paper-o: "\f256"; 355 | @fa-var-hand-peace-o: "\f25b"; 356 | @fa-var-hand-pointer-o: "\f25a"; 357 | @fa-var-hand-rock-o: "\f255"; 358 | @fa-var-hand-scissors-o: "\f257"; 359 | @fa-var-hand-spock-o: "\f259"; 360 | @fa-var-hand-stop-o: "\f256"; 361 | @fa-var-handshake-o: "\f2b5"; 362 | @fa-var-hard-of-hearing: "\f2a4"; 363 | @fa-var-hashtag: "\f292"; 364 | @fa-var-hdd-o: "\f0a0"; 365 | @fa-var-header: "\f1dc"; 366 | @fa-var-headphones: "\f025"; 367 | @fa-var-heart: "\f004"; 368 | @fa-var-heart-o: "\f08a"; 369 | @fa-var-heartbeat: "\f21e"; 370 | @fa-var-history: "\f1da"; 371 | @fa-var-home: "\f015"; 372 | @fa-var-hospital-o: "\f0f8"; 373 | @fa-var-hotel: "\f236"; 374 | @fa-var-hourglass: "\f254"; 375 | @fa-var-hourglass-1: "\f251"; 376 | @fa-var-hourglass-2: "\f252"; 377 | @fa-var-hourglass-3: "\f253"; 378 | @fa-var-hourglass-end: "\f253"; 379 | @fa-var-hourglass-half: "\f252"; 380 | @fa-var-hourglass-o: "\f250"; 381 | @fa-var-hourglass-start: "\f251"; 382 | @fa-var-houzz: "\f27c"; 383 | @fa-var-html5: "\f13b"; 384 | @fa-var-i-cursor: "\f246"; 385 | @fa-var-id-badge: "\f2c1"; 386 | @fa-var-id-card: "\f2c2"; 387 | @fa-var-id-card-o: "\f2c3"; 388 | @fa-var-ils: "\f20b"; 389 | @fa-var-image: "\f03e"; 390 | @fa-var-imdb: "\f2d8"; 391 | @fa-var-inbox: "\f01c"; 392 | @fa-var-indent: "\f03c"; 393 | @fa-var-industry: "\f275"; 394 | @fa-var-info: "\f129"; 395 | @fa-var-info-circle: "\f05a"; 396 | @fa-var-inr: "\f156"; 397 | @fa-var-instagram: "\f16d"; 398 | @fa-var-institution: "\f19c"; 399 | @fa-var-internet-explorer: "\f26b"; 400 | @fa-var-intersex: "\f224"; 401 | @fa-var-ioxhost: "\f208"; 402 | @fa-var-italic: "\f033"; 403 | @fa-var-joomla: "\f1aa"; 404 | @fa-var-jpy: "\f157"; 405 | @fa-var-jsfiddle: "\f1cc"; 406 | @fa-var-key: "\f084"; 407 | @fa-var-keyboard-o: "\f11c"; 408 | @fa-var-krw: "\f159"; 409 | @fa-var-language: "\f1ab"; 410 | @fa-var-laptop: "\f109"; 411 | @fa-var-lastfm: "\f202"; 412 | @fa-var-lastfm-square: "\f203"; 413 | @fa-var-leaf: "\f06c"; 414 | @fa-var-leanpub: "\f212"; 415 | @fa-var-legal: "\f0e3"; 416 | @fa-var-lemon-o: "\f094"; 417 | @fa-var-level-down: "\f149"; 418 | @fa-var-level-up: "\f148"; 419 | @fa-var-life-bouy: "\f1cd"; 420 | @fa-var-life-buoy: "\f1cd"; 421 | @fa-var-life-ring: "\f1cd"; 422 | @fa-var-life-saver: "\f1cd"; 423 | @fa-var-lightbulb-o: "\f0eb"; 424 | @fa-var-line-chart: "\f201"; 425 | @fa-var-link: "\f0c1"; 426 | @fa-var-linkedin: "\f0e1"; 427 | @fa-var-linkedin-square: "\f08c"; 428 | @fa-var-linode: "\f2b8"; 429 | @fa-var-linux: "\f17c"; 430 | @fa-var-list: "\f03a"; 431 | @fa-var-list-alt: "\f022"; 432 | @fa-var-list-ol: "\f0cb"; 433 | @fa-var-list-ul: "\f0ca"; 434 | @fa-var-location-arrow: "\f124"; 435 | @fa-var-lock: "\f023"; 436 | @fa-var-long-arrow-down: "\f175"; 437 | @fa-var-long-arrow-left: "\f177"; 438 | @fa-var-long-arrow-right: "\f178"; 439 | @fa-var-long-arrow-up: "\f176"; 440 | @fa-var-low-vision: "\f2a8"; 441 | @fa-var-magic: "\f0d0"; 442 | @fa-var-magnet: "\f076"; 443 | @fa-var-mail-forward: "\f064"; 444 | @fa-var-mail-reply: "\f112"; 445 | @fa-var-mail-reply-all: "\f122"; 446 | @fa-var-male: "\f183"; 447 | @fa-var-map: "\f279"; 448 | @fa-var-map-marker: "\f041"; 449 | @fa-var-map-o: "\f278"; 450 | @fa-var-map-pin: "\f276"; 451 | @fa-var-map-signs: "\f277"; 452 | @fa-var-mars: "\f222"; 453 | @fa-var-mars-double: "\f227"; 454 | @fa-var-mars-stroke: "\f229"; 455 | @fa-var-mars-stroke-h: "\f22b"; 456 | @fa-var-mars-stroke-v: "\f22a"; 457 | @fa-var-maxcdn: "\f136"; 458 | @fa-var-meanpath: "\f20c"; 459 | @fa-var-medium: "\f23a"; 460 | @fa-var-medkit: "\f0fa"; 461 | @fa-var-meetup: "\f2e0"; 462 | @fa-var-meh-o: "\f11a"; 463 | @fa-var-mercury: "\f223"; 464 | @fa-var-microchip: "\f2db"; 465 | @fa-var-microphone: "\f130"; 466 | @fa-var-microphone-slash: "\f131"; 467 | @fa-var-minus: "\f068"; 468 | @fa-var-minus-circle: "\f056"; 469 | @fa-var-minus-square: "\f146"; 470 | @fa-var-minus-square-o: "\f147"; 471 | @fa-var-mixcloud: "\f289"; 472 | @fa-var-mobile: "\f10b"; 473 | @fa-var-mobile-phone: "\f10b"; 474 | @fa-var-modx: "\f285"; 475 | @fa-var-money: "\f0d6"; 476 | @fa-var-moon-o: "\f186"; 477 | @fa-var-mortar-board: "\f19d"; 478 | @fa-var-motorcycle: "\f21c"; 479 | @fa-var-mouse-pointer: "\f245"; 480 | @fa-var-music: "\f001"; 481 | @fa-var-navicon: "\f0c9"; 482 | @fa-var-neuter: "\f22c"; 483 | @fa-var-newspaper-o: "\f1ea"; 484 | @fa-var-object-group: "\f247"; 485 | @fa-var-object-ungroup: "\f248"; 486 | @fa-var-odnoklassniki: "\f263"; 487 | @fa-var-odnoklassniki-square: "\f264"; 488 | @fa-var-opencart: "\f23d"; 489 | @fa-var-openid: "\f19b"; 490 | @fa-var-opera: "\f26a"; 491 | @fa-var-optin-monster: "\f23c"; 492 | @fa-var-outdent: "\f03b"; 493 | @fa-var-pagelines: "\f18c"; 494 | @fa-var-paint-brush: "\f1fc"; 495 | @fa-var-paper-plane: "\f1d8"; 496 | @fa-var-paper-plane-o: "\f1d9"; 497 | @fa-var-paperclip: "\f0c6"; 498 | @fa-var-paragraph: "\f1dd"; 499 | @fa-var-paste: "\f0ea"; 500 | @fa-var-pause: "\f04c"; 501 | @fa-var-pause-circle: "\f28b"; 502 | @fa-var-pause-circle-o: "\f28c"; 503 | @fa-var-paw: "\f1b0"; 504 | @fa-var-paypal: "\f1ed"; 505 | @fa-var-pencil: "\f040"; 506 | @fa-var-pencil-square: "\f14b"; 507 | @fa-var-pencil-square-o: "\f044"; 508 | @fa-var-percent: "\f295"; 509 | @fa-var-phone: "\f095"; 510 | @fa-var-phone-square: "\f098"; 511 | @fa-var-photo: "\f03e"; 512 | @fa-var-picture-o: "\f03e"; 513 | @fa-var-pie-chart: "\f200"; 514 | @fa-var-pied-piper: "\f2ae"; 515 | @fa-var-pied-piper-alt: "\f1a8"; 516 | @fa-var-pied-piper-pp: "\f1a7"; 517 | @fa-var-pinterest: "\f0d2"; 518 | @fa-var-pinterest-p: "\f231"; 519 | @fa-var-pinterest-square: "\f0d3"; 520 | @fa-var-plane: "\f072"; 521 | @fa-var-play: "\f04b"; 522 | @fa-var-play-circle: "\f144"; 523 | @fa-var-play-circle-o: "\f01d"; 524 | @fa-var-plug: "\f1e6"; 525 | @fa-var-plus: "\f067"; 526 | @fa-var-plus-circle: "\f055"; 527 | @fa-var-plus-square: "\f0fe"; 528 | @fa-var-plus-square-o: "\f196"; 529 | @fa-var-podcast: "\f2ce"; 530 | @fa-var-power-off: "\f011"; 531 | @fa-var-print: "\f02f"; 532 | @fa-var-product-hunt: "\f288"; 533 | @fa-var-puzzle-piece: "\f12e"; 534 | @fa-var-qq: "\f1d6"; 535 | @fa-var-qrcode: "\f029"; 536 | @fa-var-question: "\f128"; 537 | @fa-var-question-circle: "\f059"; 538 | @fa-var-question-circle-o: "\f29c"; 539 | @fa-var-quora: "\f2c4"; 540 | @fa-var-quote-left: "\f10d"; 541 | @fa-var-quote-right: "\f10e"; 542 | @fa-var-ra: "\f1d0"; 543 | @fa-var-random: "\f074"; 544 | @fa-var-ravelry: "\f2d9"; 545 | @fa-var-rebel: "\f1d0"; 546 | @fa-var-recycle: "\f1b8"; 547 | @fa-var-reddit: "\f1a1"; 548 | @fa-var-reddit-alien: "\f281"; 549 | @fa-var-reddit-square: "\f1a2"; 550 | @fa-var-refresh: "\f021"; 551 | @fa-var-registered: "\f25d"; 552 | @fa-var-remove: "\f00d"; 553 | @fa-var-renren: "\f18b"; 554 | @fa-var-reorder: "\f0c9"; 555 | @fa-var-repeat: "\f01e"; 556 | @fa-var-reply: "\f112"; 557 | @fa-var-reply-all: "\f122"; 558 | @fa-var-resistance: "\f1d0"; 559 | @fa-var-retweet: "\f079"; 560 | @fa-var-rmb: "\f157"; 561 | @fa-var-road: "\f018"; 562 | @fa-var-rocket: "\f135"; 563 | @fa-var-rotate-left: "\f0e2"; 564 | @fa-var-rotate-right: "\f01e"; 565 | @fa-var-rouble: "\f158"; 566 | @fa-var-rss: "\f09e"; 567 | @fa-var-rss-square: "\f143"; 568 | @fa-var-rub: "\f158"; 569 | @fa-var-ruble: "\f158"; 570 | @fa-var-rupee: "\f156"; 571 | @fa-var-s15: "\f2cd"; 572 | @fa-var-safari: "\f267"; 573 | @fa-var-save: "\f0c7"; 574 | @fa-var-scissors: "\f0c4"; 575 | @fa-var-scribd: "\f28a"; 576 | @fa-var-search: "\f002"; 577 | @fa-var-search-minus: "\f010"; 578 | @fa-var-search-plus: "\f00e"; 579 | @fa-var-sellsy: "\f213"; 580 | @fa-var-send: "\f1d8"; 581 | @fa-var-send-o: "\f1d9"; 582 | @fa-var-server: "\f233"; 583 | @fa-var-share: "\f064"; 584 | @fa-var-share-alt: "\f1e0"; 585 | @fa-var-share-alt-square: "\f1e1"; 586 | @fa-var-share-square: "\f14d"; 587 | @fa-var-share-square-o: "\f045"; 588 | @fa-var-shekel: "\f20b"; 589 | @fa-var-sheqel: "\f20b"; 590 | @fa-var-shield: "\f132"; 591 | @fa-var-ship: "\f21a"; 592 | @fa-var-shirtsinbulk: "\f214"; 593 | @fa-var-shopping-bag: "\f290"; 594 | @fa-var-shopping-basket: "\f291"; 595 | @fa-var-shopping-cart: "\f07a"; 596 | @fa-var-shower: "\f2cc"; 597 | @fa-var-sign-in: "\f090"; 598 | @fa-var-sign-language: "\f2a7"; 599 | @fa-var-sign-out: "\f08b"; 600 | @fa-var-signal: "\f012"; 601 | @fa-var-signing: "\f2a7"; 602 | @fa-var-simplybuilt: "\f215"; 603 | @fa-var-sitemap: "\f0e8"; 604 | @fa-var-skyatlas: "\f216"; 605 | @fa-var-skype: "\f17e"; 606 | @fa-var-slack: "\f198"; 607 | @fa-var-sliders: "\f1de"; 608 | @fa-var-slideshare: "\f1e7"; 609 | @fa-var-smile-o: "\f118"; 610 | @fa-var-snapchat: "\f2ab"; 611 | @fa-var-snapchat-ghost: "\f2ac"; 612 | @fa-var-snapchat-square: "\f2ad"; 613 | @fa-var-snowflake-o: "\f2dc"; 614 | @fa-var-soccer-ball-o: "\f1e3"; 615 | @fa-var-sort: "\f0dc"; 616 | @fa-var-sort-alpha-asc: "\f15d"; 617 | @fa-var-sort-alpha-desc: "\f15e"; 618 | @fa-var-sort-amount-asc: "\f160"; 619 | @fa-var-sort-amount-desc: "\f161"; 620 | @fa-var-sort-asc: "\f0de"; 621 | @fa-var-sort-desc: "\f0dd"; 622 | @fa-var-sort-down: "\f0dd"; 623 | @fa-var-sort-numeric-asc: "\f162"; 624 | @fa-var-sort-numeric-desc: "\f163"; 625 | @fa-var-sort-up: "\f0de"; 626 | @fa-var-soundcloud: "\f1be"; 627 | @fa-var-space-shuttle: "\f197"; 628 | @fa-var-spinner: "\f110"; 629 | @fa-var-spoon: "\f1b1"; 630 | @fa-var-spotify: "\f1bc"; 631 | @fa-var-square: "\f0c8"; 632 | @fa-var-square-o: "\f096"; 633 | @fa-var-stack-exchange: "\f18d"; 634 | @fa-var-stack-overflow: "\f16c"; 635 | @fa-var-star: "\f005"; 636 | @fa-var-star-half: "\f089"; 637 | @fa-var-star-half-empty: "\f123"; 638 | @fa-var-star-half-full: "\f123"; 639 | @fa-var-star-half-o: "\f123"; 640 | @fa-var-star-o: "\f006"; 641 | @fa-var-steam: "\f1b6"; 642 | @fa-var-steam-square: "\f1b7"; 643 | @fa-var-step-backward: "\f048"; 644 | @fa-var-step-forward: "\f051"; 645 | @fa-var-stethoscope: "\f0f1"; 646 | @fa-var-sticky-note: "\f249"; 647 | @fa-var-sticky-note-o: "\f24a"; 648 | @fa-var-stop: "\f04d"; 649 | @fa-var-stop-circle: "\f28d"; 650 | @fa-var-stop-circle-o: "\f28e"; 651 | @fa-var-street-view: "\f21d"; 652 | @fa-var-strikethrough: "\f0cc"; 653 | @fa-var-stumbleupon: "\f1a4"; 654 | @fa-var-stumbleupon-circle: "\f1a3"; 655 | @fa-var-subscript: "\f12c"; 656 | @fa-var-subway: "\f239"; 657 | @fa-var-suitcase: "\f0f2"; 658 | @fa-var-sun-o: "\f185"; 659 | @fa-var-superpowers: "\f2dd"; 660 | @fa-var-superscript: "\f12b"; 661 | @fa-var-support: "\f1cd"; 662 | @fa-var-table: "\f0ce"; 663 | @fa-var-tablet: "\f10a"; 664 | @fa-var-tachometer: "\f0e4"; 665 | @fa-var-tag: "\f02b"; 666 | @fa-var-tags: "\f02c"; 667 | @fa-var-tasks: "\f0ae"; 668 | @fa-var-taxi: "\f1ba"; 669 | @fa-var-telegram: "\f2c6"; 670 | @fa-var-television: "\f26c"; 671 | @fa-var-tencent-weibo: "\f1d5"; 672 | @fa-var-terminal: "\f120"; 673 | @fa-var-text-height: "\f034"; 674 | @fa-var-text-width: "\f035"; 675 | @fa-var-th: "\f00a"; 676 | @fa-var-th-large: "\f009"; 677 | @fa-var-th-list: "\f00b"; 678 | @fa-var-themeisle: "\f2b2"; 679 | @fa-var-thermometer: "\f2c7"; 680 | @fa-var-thermometer-0: "\f2cb"; 681 | @fa-var-thermometer-1: "\f2ca"; 682 | @fa-var-thermometer-2: "\f2c9"; 683 | @fa-var-thermometer-3: "\f2c8"; 684 | @fa-var-thermometer-4: "\f2c7"; 685 | @fa-var-thermometer-empty: "\f2cb"; 686 | @fa-var-thermometer-full: "\f2c7"; 687 | @fa-var-thermometer-half: "\f2c9"; 688 | @fa-var-thermometer-quarter: "\f2ca"; 689 | @fa-var-thermometer-three-quarters: "\f2c8"; 690 | @fa-var-thumb-tack: "\f08d"; 691 | @fa-var-thumbs-down: "\f165"; 692 | @fa-var-thumbs-o-down: "\f088"; 693 | @fa-var-thumbs-o-up: "\f087"; 694 | @fa-var-thumbs-up: "\f164"; 695 | @fa-var-ticket: "\f145"; 696 | @fa-var-times: "\f00d"; 697 | @fa-var-times-circle: "\f057"; 698 | @fa-var-times-circle-o: "\f05c"; 699 | @fa-var-times-rectangle: "\f2d3"; 700 | @fa-var-times-rectangle-o: "\f2d4"; 701 | @fa-var-tint: "\f043"; 702 | @fa-var-toggle-down: "\f150"; 703 | @fa-var-toggle-left: "\f191"; 704 | @fa-var-toggle-off: "\f204"; 705 | @fa-var-toggle-on: "\f205"; 706 | @fa-var-toggle-right: "\f152"; 707 | @fa-var-toggle-up: "\f151"; 708 | @fa-var-trademark: "\f25c"; 709 | @fa-var-train: "\f238"; 710 | @fa-var-transgender: "\f224"; 711 | @fa-var-transgender-alt: "\f225"; 712 | @fa-var-trash: "\f1f8"; 713 | @fa-var-trash-o: "\f014"; 714 | @fa-var-tree: "\f1bb"; 715 | @fa-var-trello: "\f181"; 716 | @fa-var-tripadvisor: "\f262"; 717 | @fa-var-trophy: "\f091"; 718 | @fa-var-truck: "\f0d1"; 719 | @fa-var-try: "\f195"; 720 | @fa-var-tty: "\f1e4"; 721 | @fa-var-tumblr: "\f173"; 722 | @fa-var-tumblr-square: "\f174"; 723 | @fa-var-turkish-lira: "\f195"; 724 | @fa-var-tv: "\f26c"; 725 | @fa-var-twitch: "\f1e8"; 726 | @fa-var-twitter: "\f099"; 727 | @fa-var-twitter-square: "\f081"; 728 | @fa-var-umbrella: "\f0e9"; 729 | @fa-var-underline: "\f0cd"; 730 | @fa-var-undo: "\f0e2"; 731 | @fa-var-universal-access: "\f29a"; 732 | @fa-var-university: "\f19c"; 733 | @fa-var-unlink: "\f127"; 734 | @fa-var-unlock: "\f09c"; 735 | @fa-var-unlock-alt: "\f13e"; 736 | @fa-var-unsorted: "\f0dc"; 737 | @fa-var-upload: "\f093"; 738 | @fa-var-usb: "\f287"; 739 | @fa-var-usd: "\f155"; 740 | @fa-var-user: "\f007"; 741 | @fa-var-user-circle: "\f2bd"; 742 | @fa-var-user-circle-o: "\f2be"; 743 | @fa-var-user-md: "\f0f0"; 744 | @fa-var-user-o: "\f2c0"; 745 | @fa-var-user-plus: "\f234"; 746 | @fa-var-user-secret: "\f21b"; 747 | @fa-var-user-times: "\f235"; 748 | @fa-var-users: "\f0c0"; 749 | @fa-var-vcard: "\f2bb"; 750 | @fa-var-vcard-o: "\f2bc"; 751 | @fa-var-venus: "\f221"; 752 | @fa-var-venus-double: "\f226"; 753 | @fa-var-venus-mars: "\f228"; 754 | @fa-var-viacoin: "\f237"; 755 | @fa-var-viadeo: "\f2a9"; 756 | @fa-var-viadeo-square: "\f2aa"; 757 | @fa-var-video-camera: "\f03d"; 758 | @fa-var-vimeo: "\f27d"; 759 | @fa-var-vimeo-square: "\f194"; 760 | @fa-var-vine: "\f1ca"; 761 | @fa-var-vk: "\f189"; 762 | @fa-var-volume-control-phone: "\f2a0"; 763 | @fa-var-volume-down: "\f027"; 764 | @fa-var-volume-off: "\f026"; 765 | @fa-var-volume-up: "\f028"; 766 | @fa-var-warning: "\f071"; 767 | @fa-var-wechat: "\f1d7"; 768 | @fa-var-weibo: "\f18a"; 769 | @fa-var-weixin: "\f1d7"; 770 | @fa-var-whatsapp: "\f232"; 771 | @fa-var-wheelchair: "\f193"; 772 | @fa-var-wheelchair-alt: "\f29b"; 773 | @fa-var-wifi: "\f1eb"; 774 | @fa-var-wikipedia-w: "\f266"; 775 | @fa-var-window-close: "\f2d3"; 776 | @fa-var-window-close-o: "\f2d4"; 777 | @fa-var-window-maximize: "\f2d0"; 778 | @fa-var-window-minimize: "\f2d1"; 779 | @fa-var-window-restore: "\f2d2"; 780 | @fa-var-windows: "\f17a"; 781 | @fa-var-won: "\f159"; 782 | @fa-var-wordpress: "\f19a"; 783 | @fa-var-wpbeginner: "\f297"; 784 | @fa-var-wpexplorer: "\f2de"; 785 | @fa-var-wpforms: "\f298"; 786 | @fa-var-wrench: "\f0ad"; 787 | @fa-var-xing: "\f168"; 788 | @fa-var-xing-square: "\f169"; 789 | @fa-var-y-combinator: "\f23b"; 790 | @fa-var-y-combinator-square: "\f1d4"; 791 | @fa-var-yahoo: "\f19e"; 792 | @fa-var-yc: "\f23b"; 793 | @fa-var-yc-square: "\f1d4"; 794 | @fa-var-yelp: "\f1e9"; 795 | @fa-var-yen: "\f157"; 796 | @fa-var-yoast: "\f2b1"; 797 | @fa-var-youtube: "\f167"; 798 | @fa-var-youtube-play: "\f16a"; 799 | @fa-var-youtube-square: "\f166"; 800 | 801 | -------------------------------------------------------------------------------- /smol/viewer/static/viewer/scss/_animated.scss: -------------------------------------------------------------------------------- 1 | // Spinning Icons 2 | // -------------------------- 3 | 4 | .#{$fa-css-prefix}-spin { 5 | -webkit-animation: fa-spin 2s infinite linear; 6 | animation: fa-spin 2s infinite linear; 7 | } 8 | 9 | .#{$fa-css-prefix}-pulse { 10 | -webkit-animation: fa-spin 1s infinite steps(8); 11 | animation: fa-spin 1s infinite steps(8); 12 | } 13 | 14 | @-webkit-keyframes fa-spin { 15 | 0% { 16 | -webkit-transform: rotate(0deg); 17 | transform: rotate(0deg); 18 | } 19 | 100% { 20 | -webkit-transform: rotate(359deg); 21 | transform: rotate(359deg); 22 | } 23 | } 24 | 25 | @keyframes fa-spin { 26 | 0% { 27 | -webkit-transform: rotate(0deg); 28 | transform: rotate(0deg); 29 | } 30 | 100% { 31 | -webkit-transform: rotate(359deg); 32 | transform: rotate(359deg); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /smol/viewer/static/viewer/scss/_bordered-pulled.scss: -------------------------------------------------------------------------------- 1 | // Bordered & Pulled 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-border { 5 | padding: .2em .25em .15em; 6 | border: solid .08em $fa-border-color; 7 | border-radius: .1em; 8 | } 9 | 10 | .#{$fa-css-prefix}-pull-left { float: left; } 11 | .#{$fa-css-prefix}-pull-right { float: right; } 12 | 13 | .#{$fa-css-prefix} { 14 | &.#{$fa-css-prefix}-pull-left { margin-right: .3em; } 15 | &.#{$fa-css-prefix}-pull-right { margin-left: .3em; } 16 | } 17 | 18 | /* Deprecated as of 4.4.0 */ 19 | .pull-right { float: right; } 20 | .pull-left { float: left; } 21 | 22 | .#{$fa-css-prefix} { 23 | &.pull-left { margin-right: .3em; } 24 | &.pull-right { margin-left: .3em; } 25 | } 26 | -------------------------------------------------------------------------------- /smol/viewer/static/viewer/scss/_core.scss: -------------------------------------------------------------------------------- 1 | // Base Class Definition 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix} { 5 | display: inline-block; 6 | font: normal normal normal #{$fa-font-size-base}/#{$fa-line-height-base} FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | 12 | } 13 | -------------------------------------------------------------------------------- /smol/viewer/static/viewer/scss/_fixed-width.scss: -------------------------------------------------------------------------------- 1 | // Fixed Width Icons 2 | // ------------------------- 3 | .#{$fa-css-prefix}-fw { 4 | width: (18em / 14); 5 | text-align: center; 6 | } 7 | -------------------------------------------------------------------------------- /smol/viewer/static/viewer/scss/_larger.scss: -------------------------------------------------------------------------------- 1 | // Icon Sizes 2 | // ------------------------- 3 | 4 | /* makes the font 33% larger relative to the icon container */ 5 | .#{$fa-css-prefix}-lg { 6 | font-size: (4em / 3); 7 | line-height: (3em / 4); 8 | vertical-align: -15%; 9 | } 10 | .#{$fa-css-prefix}-2x { font-size: 2em; } 11 | .#{$fa-css-prefix}-3x { font-size: 3em; } 12 | .#{$fa-css-prefix}-4x { font-size: 4em; } 13 | .#{$fa-css-prefix}-5x { font-size: 5em; } 14 | -------------------------------------------------------------------------------- /smol/viewer/static/viewer/scss/_list.scss: -------------------------------------------------------------------------------- 1 | // List Icons 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-ul { 5 | padding-left: 0; 6 | margin-left: $fa-li-width; 7 | list-style-type: none; 8 | > li { position: relative; } 9 | } 10 | .#{$fa-css-prefix}-li { 11 | position: absolute; 12 | left: -$fa-li-width; 13 | width: $fa-li-width; 14 | top: (2em / 14); 15 | text-align: center; 16 | &.#{$fa-css-prefix}-lg { 17 | left: -$fa-li-width + (4em / 14); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /smol/viewer/static/viewer/scss/_mixins.scss: -------------------------------------------------------------------------------- 1 | // Mixins 2 | // -------------------------- 3 | 4 | @mixin fa-icon() { 5 | display: inline-block; 6 | font: normal normal normal #{$fa-font-size-base}/#{$fa-line-height-base} FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | 12 | } 13 | 14 | @mixin fa-icon-rotate($degrees, $rotation) { 15 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation})"; 16 | -webkit-transform: rotate($degrees); 17 | -ms-transform: rotate($degrees); 18 | transform: rotate($degrees); 19 | } 20 | 21 | @mixin fa-icon-flip($horiz, $vert, $rotation) { 22 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation}, mirror=1)"; 23 | -webkit-transform: scale($horiz, $vert); 24 | -ms-transform: scale($horiz, $vert); 25 | transform: scale($horiz, $vert); 26 | } 27 | 28 | 29 | // Only display content to screen readers. A la Bootstrap 4. 30 | // 31 | // See: http://a11yproject.com/posts/how-to-hide-content/ 32 | 33 | @mixin sr-only { 34 | position: absolute; 35 | width: 1px; 36 | height: 1px; 37 | padding: 0; 38 | margin: -1px; 39 | overflow: hidden; 40 | clip: rect(0,0,0,0); 41 | border: 0; 42 | } 43 | 44 | // Use in conjunction with .sr-only to only display content when it's focused. 45 | // 46 | // Useful for "Skip to main content" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1 47 | // 48 | // Credit: HTML5 Boilerplate 49 | 50 | @mixin sr-only-focusable { 51 | &:active, 52 | &:focus { 53 | position: static; 54 | width: auto; 55 | height: auto; 56 | margin: 0; 57 | overflow: visible; 58 | clip: auto; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /smol/viewer/static/viewer/scss/_path.scss: -------------------------------------------------------------------------------- 1 | /* FONT PATH 2 | * -------------------------- */ 3 | 4 | @font-face { 5 | font-family: 'FontAwesome'; 6 | src: url('#{$fa-font-path}/fontawesome-webfont.eot?v=#{$fa-version}'); 7 | src: url('#{$fa-font-path}/fontawesome-webfont.eot?#iefix&v=#{$fa-version}') format('embedded-opentype'), 8 | url('#{$fa-font-path}/fontawesome-webfont.woff2?v=#{$fa-version}') format('woff2'), 9 | url('#{$fa-font-path}/fontawesome-webfont.woff?v=#{$fa-version}') format('woff'), 10 | url('#{$fa-font-path}/fontawesome-webfont.ttf?v=#{$fa-version}') format('truetype'), 11 | url('#{$fa-font-path}/fontawesome-webfont.svg?v=#{$fa-version}#fontawesomeregular') format('svg'); 12 | // src: url('#{$fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts 13 | font-weight: normal; 14 | font-style: normal; 15 | } 16 | -------------------------------------------------------------------------------- /smol/viewer/static/viewer/scss/_rotated-flipped.scss: -------------------------------------------------------------------------------- 1 | // Rotated & Flipped Icons 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-rotate-90 { @include fa-icon-rotate(90deg, 1); } 5 | .#{$fa-css-prefix}-rotate-180 { @include fa-icon-rotate(180deg, 2); } 6 | .#{$fa-css-prefix}-rotate-270 { @include fa-icon-rotate(270deg, 3); } 7 | 8 | .#{$fa-css-prefix}-flip-horizontal { @include fa-icon-flip(-1, 1, 0); } 9 | .#{$fa-css-prefix}-flip-vertical { @include fa-icon-flip(1, -1, 2); } 10 | 11 | // Hook for IE8-9 12 | // ------------------------- 13 | 14 | :root .#{$fa-css-prefix}-rotate-90, 15 | :root .#{$fa-css-prefix}-rotate-180, 16 | :root .#{$fa-css-prefix}-rotate-270, 17 | :root .#{$fa-css-prefix}-flip-horizontal, 18 | :root .#{$fa-css-prefix}-flip-vertical { 19 | filter: none; 20 | } 21 | -------------------------------------------------------------------------------- /smol/viewer/static/viewer/scss/_screen-reader.scss: -------------------------------------------------------------------------------- 1 | // Screen Readers 2 | // ------------------------- 3 | 4 | .sr-only { @include sr-only(); } 5 | .sr-only-focusable { @include sr-only-focusable(); } 6 | -------------------------------------------------------------------------------- /smol/viewer/static/viewer/scss/_stacked.scss: -------------------------------------------------------------------------------- 1 | // Stacked Icons 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-stack { 5 | position: relative; 6 | display: inline-block; 7 | width: 2em; 8 | height: 2em; 9 | line-height: 2em; 10 | vertical-align: middle; 11 | } 12 | .#{$fa-css-prefix}-stack-1x, .#{$fa-css-prefix}-stack-2x { 13 | position: absolute; 14 | left: 0; 15 | width: 100%; 16 | text-align: center; 17 | } 18 | .#{$fa-css-prefix}-stack-1x { line-height: inherit; } 19 | .#{$fa-css-prefix}-stack-2x { font-size: 2em; } 20 | .#{$fa-css-prefix}-inverse { color: $fa-inverse; } 21 | -------------------------------------------------------------------------------- /smol/viewer/static/viewer/scss/_variables.scss: -------------------------------------------------------------------------------- 1 | // Variables 2 | // -------------------------- 3 | 4 | $fa-font-path: "../fonts" !default; 5 | $fa-font-size-base: 14px !default; 6 | $fa-line-height-base: 1 !default; 7 | //$fa-font-path: "//netdna.bootstrapcdn.com/font-awesome/4.7.0/fonts" !default; // for referencing Bootstrap CDN font files directly 8 | $fa-css-prefix: fa !default; 9 | $fa-version: "4.7.0" !default; 10 | $fa-border-color: #eee !default; 11 | $fa-inverse: #fff !default; 12 | $fa-li-width: (30em / 14) !default; 13 | 14 | $fa-var-500px: "\f26e"; 15 | $fa-var-address-book: "\f2b9"; 16 | $fa-var-address-book-o: "\f2ba"; 17 | $fa-var-address-card: "\f2bb"; 18 | $fa-var-address-card-o: "\f2bc"; 19 | $fa-var-adjust: "\f042"; 20 | $fa-var-adn: "\f170"; 21 | $fa-var-align-center: "\f037"; 22 | $fa-var-align-justify: "\f039"; 23 | $fa-var-align-left: "\f036"; 24 | $fa-var-align-right: "\f038"; 25 | $fa-var-amazon: "\f270"; 26 | $fa-var-ambulance: "\f0f9"; 27 | $fa-var-american-sign-language-interpreting: "\f2a3"; 28 | $fa-var-anchor: "\f13d"; 29 | $fa-var-android: "\f17b"; 30 | $fa-var-angellist: "\f209"; 31 | $fa-var-angle-double-down: "\f103"; 32 | $fa-var-angle-double-left: "\f100"; 33 | $fa-var-angle-double-right: "\f101"; 34 | $fa-var-angle-double-up: "\f102"; 35 | $fa-var-angle-down: "\f107"; 36 | $fa-var-angle-left: "\f104"; 37 | $fa-var-angle-right: "\f105"; 38 | $fa-var-angle-up: "\f106"; 39 | $fa-var-apple: "\f179"; 40 | $fa-var-archive: "\f187"; 41 | $fa-var-area-chart: "\f1fe"; 42 | $fa-var-arrow-circle-down: "\f0ab"; 43 | $fa-var-arrow-circle-left: "\f0a8"; 44 | $fa-var-arrow-circle-o-down: "\f01a"; 45 | $fa-var-arrow-circle-o-left: "\f190"; 46 | $fa-var-arrow-circle-o-right: "\f18e"; 47 | $fa-var-arrow-circle-o-up: "\f01b"; 48 | $fa-var-arrow-circle-right: "\f0a9"; 49 | $fa-var-arrow-circle-up: "\f0aa"; 50 | $fa-var-arrow-down: "\f063"; 51 | $fa-var-arrow-left: "\f060"; 52 | $fa-var-arrow-right: "\f061"; 53 | $fa-var-arrow-up: "\f062"; 54 | $fa-var-arrows: "\f047"; 55 | $fa-var-arrows-alt: "\f0b2"; 56 | $fa-var-arrows-h: "\f07e"; 57 | $fa-var-arrows-v: "\f07d"; 58 | $fa-var-asl-interpreting: "\f2a3"; 59 | $fa-var-assistive-listening-systems: "\f2a2"; 60 | $fa-var-asterisk: "\f069"; 61 | $fa-var-at: "\f1fa"; 62 | $fa-var-audio-description: "\f29e"; 63 | $fa-var-automobile: "\f1b9"; 64 | $fa-var-backward: "\f04a"; 65 | $fa-var-balance-scale: "\f24e"; 66 | $fa-var-ban: "\f05e"; 67 | $fa-var-bandcamp: "\f2d5"; 68 | $fa-var-bank: "\f19c"; 69 | $fa-var-bar-chart: "\f080"; 70 | $fa-var-bar-chart-o: "\f080"; 71 | $fa-var-barcode: "\f02a"; 72 | $fa-var-bars: "\f0c9"; 73 | $fa-var-bath: "\f2cd"; 74 | $fa-var-bathtub: "\f2cd"; 75 | $fa-var-battery: "\f240"; 76 | $fa-var-battery-0: "\f244"; 77 | $fa-var-battery-1: "\f243"; 78 | $fa-var-battery-2: "\f242"; 79 | $fa-var-battery-3: "\f241"; 80 | $fa-var-battery-4: "\f240"; 81 | $fa-var-battery-empty: "\f244"; 82 | $fa-var-battery-full: "\f240"; 83 | $fa-var-battery-half: "\f242"; 84 | $fa-var-battery-quarter: "\f243"; 85 | $fa-var-battery-three-quarters: "\f241"; 86 | $fa-var-bed: "\f236"; 87 | $fa-var-beer: "\f0fc"; 88 | $fa-var-behance: "\f1b4"; 89 | $fa-var-behance-square: "\f1b5"; 90 | $fa-var-bell: "\f0f3"; 91 | $fa-var-bell-o: "\f0a2"; 92 | $fa-var-bell-slash: "\f1f6"; 93 | $fa-var-bell-slash-o: "\f1f7"; 94 | $fa-var-bicycle: "\f206"; 95 | $fa-var-binoculars: "\f1e5"; 96 | $fa-var-birthday-cake: "\f1fd"; 97 | $fa-var-bitbucket: "\f171"; 98 | $fa-var-bitbucket-square: "\f172"; 99 | $fa-var-bitcoin: "\f15a"; 100 | $fa-var-black-tie: "\f27e"; 101 | $fa-var-blind: "\f29d"; 102 | $fa-var-bluetooth: "\f293"; 103 | $fa-var-bluetooth-b: "\f294"; 104 | $fa-var-bold: "\f032"; 105 | $fa-var-bolt: "\f0e7"; 106 | $fa-var-bomb: "\f1e2"; 107 | $fa-var-book: "\f02d"; 108 | $fa-var-bookmark: "\f02e"; 109 | $fa-var-bookmark-o: "\f097"; 110 | $fa-var-braille: "\f2a1"; 111 | $fa-var-briefcase: "\f0b1"; 112 | $fa-var-btc: "\f15a"; 113 | $fa-var-bug: "\f188"; 114 | $fa-var-building: "\f1ad"; 115 | $fa-var-building-o: "\f0f7"; 116 | $fa-var-bullhorn: "\f0a1"; 117 | $fa-var-bullseye: "\f140"; 118 | $fa-var-bus: "\f207"; 119 | $fa-var-buysellads: "\f20d"; 120 | $fa-var-cab: "\f1ba"; 121 | $fa-var-calculator: "\f1ec"; 122 | $fa-var-calendar: "\f073"; 123 | $fa-var-calendar-check-o: "\f274"; 124 | $fa-var-calendar-minus-o: "\f272"; 125 | $fa-var-calendar-o: "\f133"; 126 | $fa-var-calendar-plus-o: "\f271"; 127 | $fa-var-calendar-times-o: "\f273"; 128 | $fa-var-camera: "\f030"; 129 | $fa-var-camera-retro: "\f083"; 130 | $fa-var-car: "\f1b9"; 131 | $fa-var-caret-down: "\f0d7"; 132 | $fa-var-caret-left: "\f0d9"; 133 | $fa-var-caret-right: "\f0da"; 134 | $fa-var-caret-square-o-down: "\f150"; 135 | $fa-var-caret-square-o-left: "\f191"; 136 | $fa-var-caret-square-o-right: "\f152"; 137 | $fa-var-caret-square-o-up: "\f151"; 138 | $fa-var-caret-up: "\f0d8"; 139 | $fa-var-cart-arrow-down: "\f218"; 140 | $fa-var-cart-plus: "\f217"; 141 | $fa-var-cc: "\f20a"; 142 | $fa-var-cc-amex: "\f1f3"; 143 | $fa-var-cc-diners-club: "\f24c"; 144 | $fa-var-cc-discover: "\f1f2"; 145 | $fa-var-cc-jcb: "\f24b"; 146 | $fa-var-cc-mastercard: "\f1f1"; 147 | $fa-var-cc-paypal: "\f1f4"; 148 | $fa-var-cc-stripe: "\f1f5"; 149 | $fa-var-cc-visa: "\f1f0"; 150 | $fa-var-certificate: "\f0a3"; 151 | $fa-var-chain: "\f0c1"; 152 | $fa-var-chain-broken: "\f127"; 153 | $fa-var-check: "\f00c"; 154 | $fa-var-check-circle: "\f058"; 155 | $fa-var-check-circle-o: "\f05d"; 156 | $fa-var-check-square: "\f14a"; 157 | $fa-var-check-square-o: "\f046"; 158 | $fa-var-chevron-circle-down: "\f13a"; 159 | $fa-var-chevron-circle-left: "\f137"; 160 | $fa-var-chevron-circle-right: "\f138"; 161 | $fa-var-chevron-circle-up: "\f139"; 162 | $fa-var-chevron-down: "\f078"; 163 | $fa-var-chevron-left: "\f053"; 164 | $fa-var-chevron-right: "\f054"; 165 | $fa-var-chevron-up: "\f077"; 166 | $fa-var-child: "\f1ae"; 167 | $fa-var-chrome: "\f268"; 168 | $fa-var-circle: "\f111"; 169 | $fa-var-circle-o: "\f10c"; 170 | $fa-var-circle-o-notch: "\f1ce"; 171 | $fa-var-circle-thin: "\f1db"; 172 | $fa-var-clipboard: "\f0ea"; 173 | $fa-var-clock-o: "\f017"; 174 | $fa-var-clone: "\f24d"; 175 | $fa-var-close: "\f00d"; 176 | $fa-var-cloud: "\f0c2"; 177 | $fa-var-cloud-download: "\f0ed"; 178 | $fa-var-cloud-upload: "\f0ee"; 179 | $fa-var-cny: "\f157"; 180 | $fa-var-code: "\f121"; 181 | $fa-var-code-fork: "\f126"; 182 | $fa-var-codepen: "\f1cb"; 183 | $fa-var-codiepie: "\f284"; 184 | $fa-var-coffee: "\f0f4"; 185 | $fa-var-cog: "\f013"; 186 | $fa-var-cogs: "\f085"; 187 | $fa-var-columns: "\f0db"; 188 | $fa-var-comment: "\f075"; 189 | $fa-var-comment-o: "\f0e5"; 190 | $fa-var-commenting: "\f27a"; 191 | $fa-var-commenting-o: "\f27b"; 192 | $fa-var-comments: "\f086"; 193 | $fa-var-comments-o: "\f0e6"; 194 | $fa-var-compass: "\f14e"; 195 | $fa-var-compress: "\f066"; 196 | $fa-var-connectdevelop: "\f20e"; 197 | $fa-var-contao: "\f26d"; 198 | $fa-var-copy: "\f0c5"; 199 | $fa-var-copyright: "\f1f9"; 200 | $fa-var-creative-commons: "\f25e"; 201 | $fa-var-credit-card: "\f09d"; 202 | $fa-var-credit-card-alt: "\f283"; 203 | $fa-var-crop: "\f125"; 204 | $fa-var-crosshairs: "\f05b"; 205 | $fa-var-css3: "\f13c"; 206 | $fa-var-cube: "\f1b2"; 207 | $fa-var-cubes: "\f1b3"; 208 | $fa-var-cut: "\f0c4"; 209 | $fa-var-cutlery: "\f0f5"; 210 | $fa-var-dashboard: "\f0e4"; 211 | $fa-var-dashcube: "\f210"; 212 | $fa-var-database: "\f1c0"; 213 | $fa-var-deaf: "\f2a4"; 214 | $fa-var-deafness: "\f2a4"; 215 | $fa-var-dedent: "\f03b"; 216 | $fa-var-delicious: "\f1a5"; 217 | $fa-var-desktop: "\f108"; 218 | $fa-var-deviantart: "\f1bd"; 219 | $fa-var-diamond: "\f219"; 220 | $fa-var-digg: "\f1a6"; 221 | $fa-var-dollar: "\f155"; 222 | $fa-var-dot-circle-o: "\f192"; 223 | $fa-var-download: "\f019"; 224 | $fa-var-dribbble: "\f17d"; 225 | $fa-var-drivers-license: "\f2c2"; 226 | $fa-var-drivers-license-o: "\f2c3"; 227 | $fa-var-dropbox: "\f16b"; 228 | $fa-var-drupal: "\f1a9"; 229 | $fa-var-edge: "\f282"; 230 | $fa-var-edit: "\f044"; 231 | $fa-var-eercast: "\f2da"; 232 | $fa-var-eject: "\f052"; 233 | $fa-var-ellipsis-h: "\f141"; 234 | $fa-var-ellipsis-v: "\f142"; 235 | $fa-var-empire: "\f1d1"; 236 | $fa-var-envelope: "\f0e0"; 237 | $fa-var-envelope-o: "\f003"; 238 | $fa-var-envelope-open: "\f2b6"; 239 | $fa-var-envelope-open-o: "\f2b7"; 240 | $fa-var-envelope-square: "\f199"; 241 | $fa-var-envira: "\f299"; 242 | $fa-var-eraser: "\f12d"; 243 | $fa-var-etsy: "\f2d7"; 244 | $fa-var-eur: "\f153"; 245 | $fa-var-euro: "\f153"; 246 | $fa-var-exchange: "\f0ec"; 247 | $fa-var-exclamation: "\f12a"; 248 | $fa-var-exclamation-circle: "\f06a"; 249 | $fa-var-exclamation-triangle: "\f071"; 250 | $fa-var-expand: "\f065"; 251 | $fa-var-expeditedssl: "\f23e"; 252 | $fa-var-external-link: "\f08e"; 253 | $fa-var-external-link-square: "\f14c"; 254 | $fa-var-eye: "\f06e"; 255 | $fa-var-eye-slash: "\f070"; 256 | $fa-var-eyedropper: "\f1fb"; 257 | $fa-var-fa: "\f2b4"; 258 | $fa-var-facebook: "\f09a"; 259 | $fa-var-facebook-f: "\f09a"; 260 | $fa-var-facebook-official: "\f230"; 261 | $fa-var-facebook-square: "\f082"; 262 | $fa-var-fast-backward: "\f049"; 263 | $fa-var-fast-forward: "\f050"; 264 | $fa-var-fax: "\f1ac"; 265 | $fa-var-feed: "\f09e"; 266 | $fa-var-female: "\f182"; 267 | $fa-var-fighter-jet: "\f0fb"; 268 | $fa-var-file: "\f15b"; 269 | $fa-var-file-archive-o: "\f1c6"; 270 | $fa-var-file-audio-o: "\f1c7"; 271 | $fa-var-file-code-o: "\f1c9"; 272 | $fa-var-file-excel-o: "\f1c3"; 273 | $fa-var-file-image-o: "\f1c5"; 274 | $fa-var-file-movie-o: "\f1c8"; 275 | $fa-var-file-o: "\f016"; 276 | $fa-var-file-pdf-o: "\f1c1"; 277 | $fa-var-file-photo-o: "\f1c5"; 278 | $fa-var-file-picture-o: "\f1c5"; 279 | $fa-var-file-powerpoint-o: "\f1c4"; 280 | $fa-var-file-sound-o: "\f1c7"; 281 | $fa-var-file-text: "\f15c"; 282 | $fa-var-file-text-o: "\f0f6"; 283 | $fa-var-file-video-o: "\f1c8"; 284 | $fa-var-file-word-o: "\f1c2"; 285 | $fa-var-file-zip-o: "\f1c6"; 286 | $fa-var-files-o: "\f0c5"; 287 | $fa-var-film: "\f008"; 288 | $fa-var-filter: "\f0b0"; 289 | $fa-var-fire: "\f06d"; 290 | $fa-var-fire-extinguisher: "\f134"; 291 | $fa-var-firefox: "\f269"; 292 | $fa-var-first-order: "\f2b0"; 293 | $fa-var-flag: "\f024"; 294 | $fa-var-flag-checkered: "\f11e"; 295 | $fa-var-flag-o: "\f11d"; 296 | $fa-var-flash: "\f0e7"; 297 | $fa-var-flask: "\f0c3"; 298 | $fa-var-flickr: "\f16e"; 299 | $fa-var-floppy-o: "\f0c7"; 300 | $fa-var-folder: "\f07b"; 301 | $fa-var-folder-o: "\f114"; 302 | $fa-var-folder-open: "\f07c"; 303 | $fa-var-folder-open-o: "\f115"; 304 | $fa-var-font: "\f031"; 305 | $fa-var-font-awesome: "\f2b4"; 306 | $fa-var-fonticons: "\f280"; 307 | $fa-var-fort-awesome: "\f286"; 308 | $fa-var-forumbee: "\f211"; 309 | $fa-var-forward: "\f04e"; 310 | $fa-var-foursquare: "\f180"; 311 | $fa-var-free-code-camp: "\f2c5"; 312 | $fa-var-frown-o: "\f119"; 313 | $fa-var-futbol-o: "\f1e3"; 314 | $fa-var-gamepad: "\f11b"; 315 | $fa-var-gavel: "\f0e3"; 316 | $fa-var-gbp: "\f154"; 317 | $fa-var-ge: "\f1d1"; 318 | $fa-var-gear: "\f013"; 319 | $fa-var-gears: "\f085"; 320 | $fa-var-genderless: "\f22d"; 321 | $fa-var-get-pocket: "\f265"; 322 | $fa-var-gg: "\f260"; 323 | $fa-var-gg-circle: "\f261"; 324 | $fa-var-gift: "\f06b"; 325 | $fa-var-git: "\f1d3"; 326 | $fa-var-git-square: "\f1d2"; 327 | $fa-var-github: "\f09b"; 328 | $fa-var-github-alt: "\f113"; 329 | $fa-var-github-square: "\f092"; 330 | $fa-var-gitlab: "\f296"; 331 | $fa-var-gittip: "\f184"; 332 | $fa-var-glass: "\f000"; 333 | $fa-var-glide: "\f2a5"; 334 | $fa-var-glide-g: "\f2a6"; 335 | $fa-var-globe: "\f0ac"; 336 | $fa-var-google: "\f1a0"; 337 | $fa-var-google-plus: "\f0d5"; 338 | $fa-var-google-plus-circle: "\f2b3"; 339 | $fa-var-google-plus-official: "\f2b3"; 340 | $fa-var-google-plus-square: "\f0d4"; 341 | $fa-var-google-wallet: "\f1ee"; 342 | $fa-var-graduation-cap: "\f19d"; 343 | $fa-var-gratipay: "\f184"; 344 | $fa-var-grav: "\f2d6"; 345 | $fa-var-group: "\f0c0"; 346 | $fa-var-h-square: "\f0fd"; 347 | $fa-var-hacker-news: "\f1d4"; 348 | $fa-var-hand-grab-o: "\f255"; 349 | $fa-var-hand-lizard-o: "\f258"; 350 | $fa-var-hand-o-down: "\f0a7"; 351 | $fa-var-hand-o-left: "\f0a5"; 352 | $fa-var-hand-o-right: "\f0a4"; 353 | $fa-var-hand-o-up: "\f0a6"; 354 | $fa-var-hand-paper-o: "\f256"; 355 | $fa-var-hand-peace-o: "\f25b"; 356 | $fa-var-hand-pointer-o: "\f25a"; 357 | $fa-var-hand-rock-o: "\f255"; 358 | $fa-var-hand-scissors-o: "\f257"; 359 | $fa-var-hand-spock-o: "\f259"; 360 | $fa-var-hand-stop-o: "\f256"; 361 | $fa-var-handshake-o: "\f2b5"; 362 | $fa-var-hard-of-hearing: "\f2a4"; 363 | $fa-var-hashtag: "\f292"; 364 | $fa-var-hdd-o: "\f0a0"; 365 | $fa-var-header: "\f1dc"; 366 | $fa-var-headphones: "\f025"; 367 | $fa-var-heart: "\f004"; 368 | $fa-var-heart-o: "\f08a"; 369 | $fa-var-heartbeat: "\f21e"; 370 | $fa-var-history: "\f1da"; 371 | $fa-var-home: "\f015"; 372 | $fa-var-hospital-o: "\f0f8"; 373 | $fa-var-hotel: "\f236"; 374 | $fa-var-hourglass: "\f254"; 375 | $fa-var-hourglass-1: "\f251"; 376 | $fa-var-hourglass-2: "\f252"; 377 | $fa-var-hourglass-3: "\f253"; 378 | $fa-var-hourglass-end: "\f253"; 379 | $fa-var-hourglass-half: "\f252"; 380 | $fa-var-hourglass-o: "\f250"; 381 | $fa-var-hourglass-start: "\f251"; 382 | $fa-var-houzz: "\f27c"; 383 | $fa-var-html5: "\f13b"; 384 | $fa-var-i-cursor: "\f246"; 385 | $fa-var-id-badge: "\f2c1"; 386 | $fa-var-id-card: "\f2c2"; 387 | $fa-var-id-card-o: "\f2c3"; 388 | $fa-var-ils: "\f20b"; 389 | $fa-var-image: "\f03e"; 390 | $fa-var-imdb: "\f2d8"; 391 | $fa-var-inbox: "\f01c"; 392 | $fa-var-indent: "\f03c"; 393 | $fa-var-industry: "\f275"; 394 | $fa-var-info: "\f129"; 395 | $fa-var-info-circle: "\f05a"; 396 | $fa-var-inr: "\f156"; 397 | $fa-var-instagram: "\f16d"; 398 | $fa-var-institution: "\f19c"; 399 | $fa-var-internet-explorer: "\f26b"; 400 | $fa-var-intersex: "\f224"; 401 | $fa-var-ioxhost: "\f208"; 402 | $fa-var-italic: "\f033"; 403 | $fa-var-joomla: "\f1aa"; 404 | $fa-var-jpy: "\f157"; 405 | $fa-var-jsfiddle: "\f1cc"; 406 | $fa-var-key: "\f084"; 407 | $fa-var-keyboard-o: "\f11c"; 408 | $fa-var-krw: "\f159"; 409 | $fa-var-language: "\f1ab"; 410 | $fa-var-laptop: "\f109"; 411 | $fa-var-lastfm: "\f202"; 412 | $fa-var-lastfm-square: "\f203"; 413 | $fa-var-leaf: "\f06c"; 414 | $fa-var-leanpub: "\f212"; 415 | $fa-var-legal: "\f0e3"; 416 | $fa-var-lemon-o: "\f094"; 417 | $fa-var-level-down: "\f149"; 418 | $fa-var-level-up: "\f148"; 419 | $fa-var-life-bouy: "\f1cd"; 420 | $fa-var-life-buoy: "\f1cd"; 421 | $fa-var-life-ring: "\f1cd"; 422 | $fa-var-life-saver: "\f1cd"; 423 | $fa-var-lightbulb-o: "\f0eb"; 424 | $fa-var-line-chart: "\f201"; 425 | $fa-var-link: "\f0c1"; 426 | $fa-var-linkedin: "\f0e1"; 427 | $fa-var-linkedin-square: "\f08c"; 428 | $fa-var-linode: "\f2b8"; 429 | $fa-var-linux: "\f17c"; 430 | $fa-var-list: "\f03a"; 431 | $fa-var-list-alt: "\f022"; 432 | $fa-var-list-ol: "\f0cb"; 433 | $fa-var-list-ul: "\f0ca"; 434 | $fa-var-location-arrow: "\f124"; 435 | $fa-var-lock: "\f023"; 436 | $fa-var-long-arrow-down: "\f175"; 437 | $fa-var-long-arrow-left: "\f177"; 438 | $fa-var-long-arrow-right: "\f178"; 439 | $fa-var-long-arrow-up: "\f176"; 440 | $fa-var-low-vision: "\f2a8"; 441 | $fa-var-magic: "\f0d0"; 442 | $fa-var-magnet: "\f076"; 443 | $fa-var-mail-forward: "\f064"; 444 | $fa-var-mail-reply: "\f112"; 445 | $fa-var-mail-reply-all: "\f122"; 446 | $fa-var-male: "\f183"; 447 | $fa-var-map: "\f279"; 448 | $fa-var-map-marker: "\f041"; 449 | $fa-var-map-o: "\f278"; 450 | $fa-var-map-pin: "\f276"; 451 | $fa-var-map-signs: "\f277"; 452 | $fa-var-mars: "\f222"; 453 | $fa-var-mars-double: "\f227"; 454 | $fa-var-mars-stroke: "\f229"; 455 | $fa-var-mars-stroke-h: "\f22b"; 456 | $fa-var-mars-stroke-v: "\f22a"; 457 | $fa-var-maxcdn: "\f136"; 458 | $fa-var-meanpath: "\f20c"; 459 | $fa-var-medium: "\f23a"; 460 | $fa-var-medkit: "\f0fa"; 461 | $fa-var-meetup: "\f2e0"; 462 | $fa-var-meh-o: "\f11a"; 463 | $fa-var-mercury: "\f223"; 464 | $fa-var-microchip: "\f2db"; 465 | $fa-var-microphone: "\f130"; 466 | $fa-var-microphone-slash: "\f131"; 467 | $fa-var-minus: "\f068"; 468 | $fa-var-minus-circle: "\f056"; 469 | $fa-var-minus-square: "\f146"; 470 | $fa-var-minus-square-o: "\f147"; 471 | $fa-var-mixcloud: "\f289"; 472 | $fa-var-mobile: "\f10b"; 473 | $fa-var-mobile-phone: "\f10b"; 474 | $fa-var-modx: "\f285"; 475 | $fa-var-money: "\f0d6"; 476 | $fa-var-moon-o: "\f186"; 477 | $fa-var-mortar-board: "\f19d"; 478 | $fa-var-motorcycle: "\f21c"; 479 | $fa-var-mouse-pointer: "\f245"; 480 | $fa-var-music: "\f001"; 481 | $fa-var-navicon: "\f0c9"; 482 | $fa-var-neuter: "\f22c"; 483 | $fa-var-newspaper-o: "\f1ea"; 484 | $fa-var-object-group: "\f247"; 485 | $fa-var-object-ungroup: "\f248"; 486 | $fa-var-odnoklassniki: "\f263"; 487 | $fa-var-odnoklassniki-square: "\f264"; 488 | $fa-var-opencart: "\f23d"; 489 | $fa-var-openid: "\f19b"; 490 | $fa-var-opera: "\f26a"; 491 | $fa-var-optin-monster: "\f23c"; 492 | $fa-var-outdent: "\f03b"; 493 | $fa-var-pagelines: "\f18c"; 494 | $fa-var-paint-brush: "\f1fc"; 495 | $fa-var-paper-plane: "\f1d8"; 496 | $fa-var-paper-plane-o: "\f1d9"; 497 | $fa-var-paperclip: "\f0c6"; 498 | $fa-var-paragraph: "\f1dd"; 499 | $fa-var-paste: "\f0ea"; 500 | $fa-var-pause: "\f04c"; 501 | $fa-var-pause-circle: "\f28b"; 502 | $fa-var-pause-circle-o: "\f28c"; 503 | $fa-var-paw: "\f1b0"; 504 | $fa-var-paypal: "\f1ed"; 505 | $fa-var-pencil: "\f040"; 506 | $fa-var-pencil-square: "\f14b"; 507 | $fa-var-pencil-square-o: "\f044"; 508 | $fa-var-percent: "\f295"; 509 | $fa-var-phone: "\f095"; 510 | $fa-var-phone-square: "\f098"; 511 | $fa-var-photo: "\f03e"; 512 | $fa-var-picture-o: "\f03e"; 513 | $fa-var-pie-chart: "\f200"; 514 | $fa-var-pied-piper: "\f2ae"; 515 | $fa-var-pied-piper-alt: "\f1a8"; 516 | $fa-var-pied-piper-pp: "\f1a7"; 517 | $fa-var-pinterest: "\f0d2"; 518 | $fa-var-pinterest-p: "\f231"; 519 | $fa-var-pinterest-square: "\f0d3"; 520 | $fa-var-plane: "\f072"; 521 | $fa-var-play: "\f04b"; 522 | $fa-var-play-circle: "\f144"; 523 | $fa-var-play-circle-o: "\f01d"; 524 | $fa-var-plug: "\f1e6"; 525 | $fa-var-plus: "\f067"; 526 | $fa-var-plus-circle: "\f055"; 527 | $fa-var-plus-square: "\f0fe"; 528 | $fa-var-plus-square-o: "\f196"; 529 | $fa-var-podcast: "\f2ce"; 530 | $fa-var-power-off: "\f011"; 531 | $fa-var-print: "\f02f"; 532 | $fa-var-product-hunt: "\f288"; 533 | $fa-var-puzzle-piece: "\f12e"; 534 | $fa-var-qq: "\f1d6"; 535 | $fa-var-qrcode: "\f029"; 536 | $fa-var-question: "\f128"; 537 | $fa-var-question-circle: "\f059"; 538 | $fa-var-question-circle-o: "\f29c"; 539 | $fa-var-quora: "\f2c4"; 540 | $fa-var-quote-left: "\f10d"; 541 | $fa-var-quote-right: "\f10e"; 542 | $fa-var-ra: "\f1d0"; 543 | $fa-var-random: "\f074"; 544 | $fa-var-ravelry: "\f2d9"; 545 | $fa-var-rebel: "\f1d0"; 546 | $fa-var-recycle: "\f1b8"; 547 | $fa-var-reddit: "\f1a1"; 548 | $fa-var-reddit-alien: "\f281"; 549 | $fa-var-reddit-square: "\f1a2"; 550 | $fa-var-refresh: "\f021"; 551 | $fa-var-registered: "\f25d"; 552 | $fa-var-remove: "\f00d"; 553 | $fa-var-renren: "\f18b"; 554 | $fa-var-reorder: "\f0c9"; 555 | $fa-var-repeat: "\f01e"; 556 | $fa-var-reply: "\f112"; 557 | $fa-var-reply-all: "\f122"; 558 | $fa-var-resistance: "\f1d0"; 559 | $fa-var-retweet: "\f079"; 560 | $fa-var-rmb: "\f157"; 561 | $fa-var-road: "\f018"; 562 | $fa-var-rocket: "\f135"; 563 | $fa-var-rotate-left: "\f0e2"; 564 | $fa-var-rotate-right: "\f01e"; 565 | $fa-var-rouble: "\f158"; 566 | $fa-var-rss: "\f09e"; 567 | $fa-var-rss-square: "\f143"; 568 | $fa-var-rub: "\f158"; 569 | $fa-var-ruble: "\f158"; 570 | $fa-var-rupee: "\f156"; 571 | $fa-var-s15: "\f2cd"; 572 | $fa-var-safari: "\f267"; 573 | $fa-var-save: "\f0c7"; 574 | $fa-var-scissors: "\f0c4"; 575 | $fa-var-scribd: "\f28a"; 576 | $fa-var-search: "\f002"; 577 | $fa-var-search-minus: "\f010"; 578 | $fa-var-search-plus: "\f00e"; 579 | $fa-var-sellsy: "\f213"; 580 | $fa-var-send: "\f1d8"; 581 | $fa-var-send-o: "\f1d9"; 582 | $fa-var-server: "\f233"; 583 | $fa-var-share: "\f064"; 584 | $fa-var-share-alt: "\f1e0"; 585 | $fa-var-share-alt-square: "\f1e1"; 586 | $fa-var-share-square: "\f14d"; 587 | $fa-var-share-square-o: "\f045"; 588 | $fa-var-shekel: "\f20b"; 589 | $fa-var-sheqel: "\f20b"; 590 | $fa-var-shield: "\f132"; 591 | $fa-var-ship: "\f21a"; 592 | $fa-var-shirtsinbulk: "\f214"; 593 | $fa-var-shopping-bag: "\f290"; 594 | $fa-var-shopping-basket: "\f291"; 595 | $fa-var-shopping-cart: "\f07a"; 596 | $fa-var-shower: "\f2cc"; 597 | $fa-var-sign-in: "\f090"; 598 | $fa-var-sign-language: "\f2a7"; 599 | $fa-var-sign-out: "\f08b"; 600 | $fa-var-signal: "\f012"; 601 | $fa-var-signing: "\f2a7"; 602 | $fa-var-simplybuilt: "\f215"; 603 | $fa-var-sitemap: "\f0e8"; 604 | $fa-var-skyatlas: "\f216"; 605 | $fa-var-skype: "\f17e"; 606 | $fa-var-slack: "\f198"; 607 | $fa-var-sliders: "\f1de"; 608 | $fa-var-slideshare: "\f1e7"; 609 | $fa-var-smile-o: "\f118"; 610 | $fa-var-snapchat: "\f2ab"; 611 | $fa-var-snapchat-ghost: "\f2ac"; 612 | $fa-var-snapchat-square: "\f2ad"; 613 | $fa-var-snowflake-o: "\f2dc"; 614 | $fa-var-soccer-ball-o: "\f1e3"; 615 | $fa-var-sort: "\f0dc"; 616 | $fa-var-sort-alpha-asc: "\f15d"; 617 | $fa-var-sort-alpha-desc: "\f15e"; 618 | $fa-var-sort-amount-asc: "\f160"; 619 | $fa-var-sort-amount-desc: "\f161"; 620 | $fa-var-sort-asc: "\f0de"; 621 | $fa-var-sort-desc: "\f0dd"; 622 | $fa-var-sort-down: "\f0dd"; 623 | $fa-var-sort-numeric-asc: "\f162"; 624 | $fa-var-sort-numeric-desc: "\f163"; 625 | $fa-var-sort-up: "\f0de"; 626 | $fa-var-soundcloud: "\f1be"; 627 | $fa-var-space-shuttle: "\f197"; 628 | $fa-var-spinner: "\f110"; 629 | $fa-var-spoon: "\f1b1"; 630 | $fa-var-spotify: "\f1bc"; 631 | $fa-var-square: "\f0c8"; 632 | $fa-var-square-o: "\f096"; 633 | $fa-var-stack-exchange: "\f18d"; 634 | $fa-var-stack-overflow: "\f16c"; 635 | $fa-var-star: "\f005"; 636 | $fa-var-star-half: "\f089"; 637 | $fa-var-star-half-empty: "\f123"; 638 | $fa-var-star-half-full: "\f123"; 639 | $fa-var-star-half-o: "\f123"; 640 | $fa-var-star-o: "\f006"; 641 | $fa-var-steam: "\f1b6"; 642 | $fa-var-steam-square: "\f1b7"; 643 | $fa-var-step-backward: "\f048"; 644 | $fa-var-step-forward: "\f051"; 645 | $fa-var-stethoscope: "\f0f1"; 646 | $fa-var-sticky-note: "\f249"; 647 | $fa-var-sticky-note-o: "\f24a"; 648 | $fa-var-stop: "\f04d"; 649 | $fa-var-stop-circle: "\f28d"; 650 | $fa-var-stop-circle-o: "\f28e"; 651 | $fa-var-street-view: "\f21d"; 652 | $fa-var-strikethrough: "\f0cc"; 653 | $fa-var-stumbleupon: "\f1a4"; 654 | $fa-var-stumbleupon-circle: "\f1a3"; 655 | $fa-var-subscript: "\f12c"; 656 | $fa-var-subway: "\f239"; 657 | $fa-var-suitcase: "\f0f2"; 658 | $fa-var-sun-o: "\f185"; 659 | $fa-var-superpowers: "\f2dd"; 660 | $fa-var-superscript: "\f12b"; 661 | $fa-var-support: "\f1cd"; 662 | $fa-var-table: "\f0ce"; 663 | $fa-var-tablet: "\f10a"; 664 | $fa-var-tachometer: "\f0e4"; 665 | $fa-var-tag: "\f02b"; 666 | $fa-var-tags: "\f02c"; 667 | $fa-var-tasks: "\f0ae"; 668 | $fa-var-taxi: "\f1ba"; 669 | $fa-var-telegram: "\f2c6"; 670 | $fa-var-television: "\f26c"; 671 | $fa-var-tencent-weibo: "\f1d5"; 672 | $fa-var-terminal: "\f120"; 673 | $fa-var-text-height: "\f034"; 674 | $fa-var-text-width: "\f035"; 675 | $fa-var-th: "\f00a"; 676 | $fa-var-th-large: "\f009"; 677 | $fa-var-th-list: "\f00b"; 678 | $fa-var-themeisle: "\f2b2"; 679 | $fa-var-thermometer: "\f2c7"; 680 | $fa-var-thermometer-0: "\f2cb"; 681 | $fa-var-thermometer-1: "\f2ca"; 682 | $fa-var-thermometer-2: "\f2c9"; 683 | $fa-var-thermometer-3: "\f2c8"; 684 | $fa-var-thermometer-4: "\f2c7"; 685 | $fa-var-thermometer-empty: "\f2cb"; 686 | $fa-var-thermometer-full: "\f2c7"; 687 | $fa-var-thermometer-half: "\f2c9"; 688 | $fa-var-thermometer-quarter: "\f2ca"; 689 | $fa-var-thermometer-three-quarters: "\f2c8"; 690 | $fa-var-thumb-tack: "\f08d"; 691 | $fa-var-thumbs-down: "\f165"; 692 | $fa-var-thumbs-o-down: "\f088"; 693 | $fa-var-thumbs-o-up: "\f087"; 694 | $fa-var-thumbs-up: "\f164"; 695 | $fa-var-ticket: "\f145"; 696 | $fa-var-times: "\f00d"; 697 | $fa-var-times-circle: "\f057"; 698 | $fa-var-times-circle-o: "\f05c"; 699 | $fa-var-times-rectangle: "\f2d3"; 700 | $fa-var-times-rectangle-o: "\f2d4"; 701 | $fa-var-tint: "\f043"; 702 | $fa-var-toggle-down: "\f150"; 703 | $fa-var-toggle-left: "\f191"; 704 | $fa-var-toggle-off: "\f204"; 705 | $fa-var-toggle-on: "\f205"; 706 | $fa-var-toggle-right: "\f152"; 707 | $fa-var-toggle-up: "\f151"; 708 | $fa-var-trademark: "\f25c"; 709 | $fa-var-train: "\f238"; 710 | $fa-var-transgender: "\f224"; 711 | $fa-var-transgender-alt: "\f225"; 712 | $fa-var-trash: "\f1f8"; 713 | $fa-var-trash-o: "\f014"; 714 | $fa-var-tree: "\f1bb"; 715 | $fa-var-trello: "\f181"; 716 | $fa-var-tripadvisor: "\f262"; 717 | $fa-var-trophy: "\f091"; 718 | $fa-var-truck: "\f0d1"; 719 | $fa-var-try: "\f195"; 720 | $fa-var-tty: "\f1e4"; 721 | $fa-var-tumblr: "\f173"; 722 | $fa-var-tumblr-square: "\f174"; 723 | $fa-var-turkish-lira: "\f195"; 724 | $fa-var-tv: "\f26c"; 725 | $fa-var-twitch: "\f1e8"; 726 | $fa-var-twitter: "\f099"; 727 | $fa-var-twitter-square: "\f081"; 728 | $fa-var-umbrella: "\f0e9"; 729 | $fa-var-underline: "\f0cd"; 730 | $fa-var-undo: "\f0e2"; 731 | $fa-var-universal-access: "\f29a"; 732 | $fa-var-university: "\f19c"; 733 | $fa-var-unlink: "\f127"; 734 | $fa-var-unlock: "\f09c"; 735 | $fa-var-unlock-alt: "\f13e"; 736 | $fa-var-unsorted: "\f0dc"; 737 | $fa-var-upload: "\f093"; 738 | $fa-var-usb: "\f287"; 739 | $fa-var-usd: "\f155"; 740 | $fa-var-user: "\f007"; 741 | $fa-var-user-circle: "\f2bd"; 742 | $fa-var-user-circle-o: "\f2be"; 743 | $fa-var-user-md: "\f0f0"; 744 | $fa-var-user-o: "\f2c0"; 745 | $fa-var-user-plus: "\f234"; 746 | $fa-var-user-secret: "\f21b"; 747 | $fa-var-user-times: "\f235"; 748 | $fa-var-users: "\f0c0"; 749 | $fa-var-vcard: "\f2bb"; 750 | $fa-var-vcard-o: "\f2bc"; 751 | $fa-var-venus: "\f221"; 752 | $fa-var-venus-double: "\f226"; 753 | $fa-var-venus-mars: "\f228"; 754 | $fa-var-viacoin: "\f237"; 755 | $fa-var-viadeo: "\f2a9"; 756 | $fa-var-viadeo-square: "\f2aa"; 757 | $fa-var-video-camera: "\f03d"; 758 | $fa-var-vimeo: "\f27d"; 759 | $fa-var-vimeo-square: "\f194"; 760 | $fa-var-vine: "\f1ca"; 761 | $fa-var-vk: "\f189"; 762 | $fa-var-volume-control-phone: "\f2a0"; 763 | $fa-var-volume-down: "\f027"; 764 | $fa-var-volume-off: "\f026"; 765 | $fa-var-volume-up: "\f028"; 766 | $fa-var-warning: "\f071"; 767 | $fa-var-wechat: "\f1d7"; 768 | $fa-var-weibo: "\f18a"; 769 | $fa-var-weixin: "\f1d7"; 770 | $fa-var-whatsapp: "\f232"; 771 | $fa-var-wheelchair: "\f193"; 772 | $fa-var-wheelchair-alt: "\f29b"; 773 | $fa-var-wifi: "\f1eb"; 774 | $fa-var-wikipedia-w: "\f266"; 775 | $fa-var-window-close: "\f2d3"; 776 | $fa-var-window-close-o: "\f2d4"; 777 | $fa-var-window-maximize: "\f2d0"; 778 | $fa-var-window-minimize: "\f2d1"; 779 | $fa-var-window-restore: "\f2d2"; 780 | $fa-var-windows: "\f17a"; 781 | $fa-var-won: "\f159"; 782 | $fa-var-wordpress: "\f19a"; 783 | $fa-var-wpbeginner: "\f297"; 784 | $fa-var-wpexplorer: "\f2de"; 785 | $fa-var-wpforms: "\f298"; 786 | $fa-var-wrench: "\f0ad"; 787 | $fa-var-xing: "\f168"; 788 | $fa-var-xing-square: "\f169"; 789 | $fa-var-y-combinator: "\f23b"; 790 | $fa-var-y-combinator-square: "\f1d4"; 791 | $fa-var-yahoo: "\f19e"; 792 | $fa-var-yc: "\f23b"; 793 | $fa-var-yc-square: "\f1d4"; 794 | $fa-var-yelp: "\f1e9"; 795 | $fa-var-yen: "\f157"; 796 | $fa-var-yoast: "\f2b1"; 797 | $fa-var-youtube: "\f167"; 798 | $fa-var-youtube-play: "\f16a"; 799 | $fa-var-youtube-square: "\f166"; 800 | 801 | -------------------------------------------------------------------------------- /smol/viewer/static/viewer/scss/font-awesome.scss: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome 3 | * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) 4 | */ 5 | 6 | @import "variables"; 7 | @import "mixins"; 8 | @import "path"; 9 | @import "core"; 10 | @import "larger"; 11 | @import "fixed-width"; 12 | @import "list"; 13 | @import "bordered-pulled"; 14 | @import "animated"; 15 | @import "rotated-flipped"; 16 | @import "stacked"; 17 | @import "icons"; 18 | @import "screen-reader"; 19 | -------------------------------------------------------------------------------- /smol/viewer/templates/viewer/base.html: -------------------------------------------------------------------------------- 1 | {% autoescape off %} 2 | {% load humanize %} 3 | {% load static %} 4 | {% load cache %} 5 | {% load custom_filters %} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | {% block head %} 15 | smol 16 | {% endblock %} 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 80 | {% block content %} 81 | 82 | {% endblock %} 83 | 84 | {% block script %} 85 | 86 | {% endblock %} 87 | 88 | 89 | 90 | {% endautoescape %} -------------------------------------------------------------------------------- /smol/viewer/templates/viewer/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'viewer/base.html' %} {% load custom_filters %} {% load humanize %} 2 | {% load static %} {% block content %} 3 |
4 | {% include "viewer/video_gallery.html" with title="Videos:" type="videos" %} 5 | {% include "viewer/video_gallery.html" with title="Images:" type="images" %} 6 |
7 | {% endblock %} {% block script %} 8 | 9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /smol/viewer/templates/viewer/labels.html: -------------------------------------------------------------------------------- 1 | {% extends 'viewer/base.html' %} 2 | 3 | {% block content %} 4 |
5 |
6 |
7 |
{% csrf_token %} 8 | {{form}} 9 | 10 |
11 | {% if error %} 12 | {{error}} 13 | {% endif %} 14 |
15 |
16 | {% for label in object_list %} 17 | 25 | {% endfor %} 26 |
27 |
28 |
29 | {% endblock %} -------------------------------------------------------------------------------- /smol/viewer/templates/viewer/loader.html: -------------------------------------------------------------------------------- 1 | {% extends 'viewer/base.html' %} {% load humanize %} {% load static %} {% block content %} 2 |
3 |
4 |
5 |
6 |

Video Count:

7 |
{{ video_count }}
8 |

Image Count:

9 |
{{ image_count }}
10 |
11 |
12 |
13 | 16 |

17 |

18 |
19 |
20 | 21 | 22 |
23 |
24 |
25 | 26 | {% endblock %} {% block script %} 27 | 28 | {% endblock %} 29 | -------------------------------------------------------------------------------- /smol/viewer/templates/viewer/search.html: -------------------------------------------------------------------------------- 1 | {% extends 'viewer/base.html' %} 2 | {% load custom_filters %} 3 | {% block content %} 4 |
5 | {% if videos %} 6 | {% include "viewer/carousel.html" with id="carousel1" title="Videos" result_set=videos type="videos" dataframes=15 %} 7 | {% endif %} 8 | {% if labels %} 9 | {% include "viewer/carousel.html" with id="carousel2" title="Labels" result_set=labels type="videos" dataframes=15 %} 10 | {% endif %} 11 | {% if images %} 12 | {% include "viewer/carousel.html" with id="carousel3" title="Images" result_set=images type="images" dataframes=15 %} 13 | {% endif %} 14 | 15 |
16 | {% endblock %} -------------------------------------------------------------------------------- /smol/viewer/templates/viewer/video.html: -------------------------------------------------------------------------------- 1 | {% extends 'viewer/base.html' %} 2 | {% load humanize %} 3 | {% load static %} 4 | {% load custom_filters %} 5 | {% block head %} 6 | smol 7 | {% endblock %} 8 | {% block content %} 9 |
10 | 49 | 50 | {% if video.related_videos.all|length >= 1 %} 51 |
52 | {% include "viewer/video_gallery.html" with title="Recognition related videos:" videos=video.related_videos.all|order_filter:"to_video__score" type="videos" %} 53 |
54 | {% endif %} 55 | {% if recommendations|length > 1 %} 56 |
57 | {% include "viewer/video_gallery.html" with title="Recommendations:" videos=recommendations type="videos" %} 58 |
59 | {% endif %} 60 |
61 | {% endblock %} 62 | 63 | {% block script %} 64 | 65 | {% endblock %} 66 | -------------------------------------------------------------------------------- /smol/viewer/templates/viewer/video_gallery.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | {% load custom_filters %} 3 |

4 | 5 |
6 | {{ title }} ({{videos.all|length}}): 7 |
8 |
9 |

10 |
11 | {% if videos|length >= 1 and type == "videos" %} 12 | {% for video in videos %} 13 | 23 | {% endfor %} 24 | {% endif %} 25 | {% if images|length >= 1 and type == "images" %} 26 | {% for image in images %} 27 | 28 | {% endfor %} 29 | {% endif %} 30 |
-------------------------------------------------------------------------------- /smol/viewer/templates/viewer/videos_page.html: -------------------------------------------------------------------------------- 1 | {% extends 'viewer/base.html' %} {% load custom_filters %} {% load humanize %} 2 | {% load static %} {% block content %} 3 |
4 |
5 |
{% csrf_token %} {{form}}
6 | 7 |
8 | {% include "viewer/video_gallery.html" with title="Videos:" type="videos" %} 9 |
10 | {% endblock %} {% block script %} 11 | 12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /smol/viewer/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EinAeffchen/Smol/2ee58c4647973818bb3c124f70a92b71e3b7f5c0/smol/viewer/templatetags/__init__.py -------------------------------------------------------------------------------- /smol/viewer/templatetags/custom_filters.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from datetime import timedelta 3 | from urllib.parse import quote 4 | from pathlib import Path 5 | 6 | register = template.Library() 7 | 8 | 9 | @register.filter 10 | def rem_slashes(value): 11 | return value.replace("/", "") 12 | 13 | 14 | @register.filter 15 | def order_filter(value, order): 16 | return value.order_by(order) 17 | 18 | 19 | @register.filter 20 | def get_type(value): 21 | return value.split(".")[-1] 22 | 23 | 24 | @register.filter 25 | def human_duration(value): 26 | return str(timedelta(seconds=round(value))) 27 | 28 | 29 | @register.filter 30 | def hours(value): 31 | if value: 32 | return value // 3600 33 | else: 34 | return "" 35 | 36 | 37 | @register.filter 38 | def minutes(value): 39 | if value: 40 | return value // 60 % 60 41 | else: 42 | return "" 43 | 44 | 45 | @register.filter 46 | def urlencode(value): 47 | return quote(value) 48 | 49 | 50 | @register.filter 51 | def connect(first, second): 52 | return str(first) + str(second) 53 | 54 | 55 | @register.filter 56 | def to_duration(value_in_seconds): 57 | minutes = value_in_seconds / 60 58 | seconds = value_in_seconds % 60 59 | return f"{int(minutes):02d}:{int(seconds):02d}" 60 | 61 | 62 | @register.filter 63 | def get_type(value): 64 | mime_mapping = { 65 | ".avi": "video/x-msvideo", 66 | ".mp4": "video/mp4", 67 | ".mpeg": "video/mpeg", 68 | ".mpkg": "application/vnd.apple.installer+xml", 69 | ".ts": "video/mp2t", 70 | ".wav": "audio/wav", 71 | ".webm": "video/webm", 72 | ".3gp": "video/3gpp", 73 | ".mkv": "video/webm", 74 | } 75 | extension = Path(value).suffix 76 | return mime_mapping.get(extension, "video/mp4") 77 | -------------------------------------------------------------------------------- /smol/viewer/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /smol/viewer/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import views 4 | 5 | urlpatterns = [ 6 | path("", views.IndexView.as_view(), name="index"), 7 | path("addVideoLabel/", views.add_video_label, name="label-video-add"), 8 | path( 9 | "deleteVideoLabel/", 10 | views.delete_video_label, 11 | name="label-video-delete", 12 | ), 13 | path("fav//", views.add_favorite, name="add_favorites"), 14 | path( 15 | "fav_image//", 16 | views.add_favorite_image, 17 | name="add_favorite_image", 18 | ), 19 | path( 20 | "label//", 21 | views.LabelResultView.as_view(), 22 | name="label-result-view", 23 | ), 24 | path("label//delete/", views.delete_label, name="label-delete"), 25 | path("labels/", views.LabelView.as_view(), name="labels"), 26 | path("load/", views.DataLoader.as_view(), name="load"), 27 | path("load-video/", views.load_file, name="load-file"), 28 | path("get-new-videos/", views.get_new_files, name="get-new-files"), 29 | path("cleanData/", views.clean_data, name="clean-data"), 30 | path("analyze/", views.scan_video, name="analyze-video"), 31 | path("delete-encoding/", views.delete_encoding, name="delete-encoding"), 32 | path("remfav//", views.rem_favorite, name="rem_favorites"), 33 | path( 34 | "remfav_image//", 35 | views.rem_favorite_image, 36 | name="rem_favorite_image", 37 | ), 38 | path("remvid/", views.rem_video, name="rem_video"), 39 | path("remmeta/", views.rem_meta, name="rem_video_meta"), 40 | path("remimage/", views.rem_image, name="rem-image"), 41 | path("search/", views.SearchView.as_view(), name="search"), 42 | path("video//", views.VideoView.as_view(), name="video"), 43 | path( 44 | "videoOverview/", views.VideoList.as_view(), name="video-overview" 45 | ), 46 | path( 47 | "imageOverview/", views.VideoList.as_view(), name="image-overview" 48 | ), 49 | ] 50 | -------------------------------------------------------------------------------- /smol/viewer/utils.py: -------------------------------------------------------------------------------- 1 | from PIL import Image 2 | from pathlib import Path 3 | import base64 4 | 5 | Image.MAX_IMAGE_PIXELS = None 6 | 7 | def split_image(path: Path, parts: int = 100): 8 | images = [] 9 | im = Image.open(path) 10 | imgwidth, imgheight = im.size 11 | crop_width = int(imgwidth / parts) 12 | for i in range(0, imgwidth-crop_width, crop_width): 13 | box = (i, 0, i+crop_width, imgheight) 14 | a = im.crop(box) 15 | a = a.convert("RGB") 16 | images.append(a) 17 | return images 18 | 19 | def base64_encode(string: str): 20 | string_bytes = string.encode("utf-8") 21 | base64_bytes = base64.b64encode(string_bytes) 22 | base64_string = base64_bytes.decode("utf-8") 23 | return base64_string 24 | 25 | def base64_decode(string: str): 26 | string_bytes = string.encode("utf-8") 27 | base64_bytes = base64.b64decode(string_bytes) 28 | string_data = base64_bytes.decode("utf-8") 29 | return string_data -------------------------------------------------------------------------------- /smol/viewer/video_processor.py: -------------------------------------------------------------------------------- 1 | import json 2 | import pickle 3 | import subprocess 4 | from datetime import datetime 5 | from hashlib import sha1 6 | from pathlib import Path 7 | from typing import Generator, Optional 8 | 9 | import ffmpeg 10 | import pandas as pd 11 | from deepface.modules import verification 12 | from django.conf import settings 13 | from PIL import Image as PILImage 14 | from tqdm import tqdm 15 | 16 | from .models import Label, Video, VideoPersonMatch 17 | 18 | 19 | def get_duration(stream: dict): 20 | duration = stream.get("duration") # mp4 21 | if not duration and stream.get("tags"): 22 | duration = stream.get("tags", {}).get("DURATION-eng") # mkv 23 | if not duration: 24 | duration = stream.get("tags", {}).get("DURATION") # webm 25 | return duration 26 | 27 | 28 | def read_image_info(path: Path, file_path: Path): 29 | img = PILImage.open(str(path)) 30 | 31 | image_data = dict() 32 | image_data["dim_height"], image_data["dim_width"] = img.size 33 | image_data["size"] = path.stat().st_size 34 | image_data["path"] = file_path 35 | return image_data 36 | 37 | 38 | def read_video_info(path: Path) -> dict: 39 | probe = ffmpeg.probe(str(path)) 40 | probe_str = json.dumps(probe, indent=2, sort_keys=True) 41 | video_stream = next( 42 | ( 43 | stream 44 | for stream in probe["streams"] 45 | if stream["codec_type"] == "video" 46 | ), 47 | None, 48 | ) 49 | audio_stream = next( 50 | ( 51 | stream 52 | for stream in probe["streams"] 53 | if stream["codec_type"] == "audio" 54 | ), 55 | None, 56 | ) 57 | video_data = dict() 58 | video_data["id_hash"] = sha1(probe_str.encode("utf-8")).hexdigest() 59 | video_data["dim_height"] = video_stream["height"] 60 | video_data["dim_width"] = video_stream["width"] 61 | video_data["videocodec"] = video_stream["codec_name"] 62 | if audio_stream: 63 | video_data["audiocodec"] = audio_stream["codec_name"] 64 | video_data["duration"] = get_duration(video_stream) 65 | video_data["bitrate"] = video_stream.get("bit_rate") 66 | try: 67 | video_data["duration"] = float(video_data["duration"]) 68 | except ValueError: 69 | time = video_data["duration"].split(".")[0] 70 | dur = datetime.strptime(time, "%H:%M:%S") - datetime(1900, 1, 1) 71 | video_data["duration"] = dur.total_seconds() 72 | except TypeError: 73 | video_data["duration"] = 0 74 | return video_data 75 | 76 | 77 | def calculate_padding(width: int, height: int) -> str: 78 | tgt_width = 410 # (max dim / frames) 79 | tgt_height = tgt_width / 1.51 80 | height_padding = 0 81 | # scale to target height 82 | diff = height / tgt_height 83 | width = width / diff 84 | if width > tgt_width: 85 | diff = width / tgt_width 86 | width = tgt_width 87 | tgt_height_new = tgt_height / diff 88 | height_padding = int((tgt_height - tgt_height_new)) 89 | tgt_height = tgt_height_new 90 | 91 | padding = tgt_width - width 92 | 93 | if padding < 0: 94 | pad = f"scale={int(width)-10}:{int(tgt_height)-10}:force_original_aspect_ratio=decrease,pad={int(tgt_width)}:{int(tgt_height)+height_padding}:10:{int(height_padding/2)}:black" 95 | else: 96 | pad = f"scale={int(width)}:{int(tgt_height)-10}:force_original_aspect_ratio=decrease,pad={int(tgt_width)}:{int(tgt_height)+height_padding}:{int(padding/2)}:{int(height_padding/2)}:black" 97 | return pad 98 | 99 | 100 | def update_preview_name(filename: str, preview_dir: Path): 101 | counter = 1 102 | out_filename = f"{filename}_{counter}.jpg" 103 | out_path = preview_dir / out_filename 104 | while out_path.is_file(): 105 | counter += 1 106 | out_filename = f"{filename}_{counter}.jpg" 107 | out_path = preview_dir / out_filename 108 | return out_path, out_filename 109 | 110 | 111 | def generate_preview(video: Video, frames: int, video_path: Path) -> str: 112 | if frames: 113 | nth_frame = int(int(frames) / settings.PREVIEW_IMAGES) 114 | else: 115 | nth_frame = settings.PREVIEW_IMAGES 116 | out_filename = f"{video.filename}-{video.duration}.jpg" 117 | out_path = settings.PREVIEW_DIR / out_filename 118 | if out_path.is_file(): 119 | return out_filename 120 | pad = calculate_padding(video.dim_width, video.dim_height) 121 | if nth_frame > 100: 122 | nth_frame = 100 123 | process = subprocess.Popen( 124 | [ 125 | "ffmpeg", 126 | "-loglevel", 127 | "panic", 128 | "-y", 129 | "-i", 130 | str(video_path), 131 | "-frames", 132 | "1", 133 | "-q:v", 134 | "5", 135 | "-c:v", 136 | "mjpeg", 137 | "-vf", 138 | f"select=not(mod(n\,{nth_frame})),{pad},tile={settings.PREVIEW_IMAGES}x1", 139 | str(out_path), 140 | ], 141 | stdout=subprocess.PIPE, 142 | stderr=subprocess.PIPE, 143 | ) 144 | process.wait() 145 | # process.terminate() 146 | return out_filename 147 | 148 | 149 | def generate_thumbnail(video: "Video", video_path: Path) -> str: 150 | out_filename = f"{video.id_hash}.jpg" 151 | out_path = settings.THUMBNAIL_DIR / out_filename 152 | if out_path.is_file(): 153 | return out_filename 154 | if not out_path.is_file(): 155 | if video.duration: 156 | thumbnail_ss = int(video.duration / 2) 157 | else: 158 | thumbnail_ss = 5 159 | process = subprocess.Popen( 160 | [ 161 | "ffmpeg", 162 | "-ss", 163 | str(thumbnail_ss), 164 | "-loglevel", 165 | "panic", 166 | "-y", 167 | "-i", 168 | str(video_path), 169 | "-frames", 170 | "1", 171 | "-q:v", 172 | "0", 173 | "-an", 174 | "-c:v", 175 | "mjpeg", 176 | "-vf", 177 | "scale=-2:380:force_original_aspect_ratio=increase", 178 | str(out_path), 179 | ], 180 | stdout=subprocess.PIPE, 181 | stderr=subprocess.PIPE, 182 | ) 183 | process.wait() 184 | return out_filename 185 | 186 | 187 | def add_labels_by_path(video: "Video"): 188 | for part in Path(video.path).parts[:-1]: 189 | label_candidates = part.lower() 190 | for label_candidate in label_candidates.split(): 191 | try: 192 | label = Label.objects.get(label=label_candidate) 193 | except Label.DoesNotExist: 194 | label = Label.objects.create(label=label_candidate) 195 | label.save() 196 | video.labels.add(label) 197 | 198 | 199 | def load_embedding_database(video_id: Optional[int] = None): 200 | representations: Generator[Path, None, None] = ( 201 | settings.RECOGNITION_DATA_PATH.glob("*.pkl") 202 | ) 203 | datasets = [] 204 | print("Loading recognition db!") 205 | for rep in representations: 206 | if video_id and rep.stem == str(video_id): 207 | continue 208 | with open(rep, "rb") as f_in: 209 | faces = pickle.load(f_in) 210 | datasets += faces 211 | return pd.DataFrame(datasets) 212 | 213 | 214 | face_database = load_embedding_database() 215 | 216 | 217 | class Matcher: 218 | MATCHING_MODES: dict[int, callable] 219 | 220 | def __init__(self) -> None: 221 | self.MATCHING_MODES = dict() 222 | self.MATCHING_MODES[1] = self.matching_mode_1 223 | self.MATCHING_MODES[2] = self.matching_mode_2 224 | 225 | def start_matching(self, video: Video, mode: int = 1): 226 | if video.ran_recognition: 227 | print("Video already got checked for related videos!") 228 | return 229 | encodings = video.face_encodings 230 | if not encodings or len(encodings) == 0: 231 | return 232 | if face_database.empty: 233 | print("No other detection data exists, nothing to match against!") 234 | return 235 | return self.MATCHING_MODES[mode](video) 236 | 237 | def matching_mode_2( 238 | self, 239 | video: Video, 240 | distance_metric: str = "cosine", 241 | ): 242 | """ 243 | Takes into account all distances between the src_video 244 | first face and averages the distance to all other faces.""" 245 | matched_videos = dict() 246 | first_encoding = video.face_encodings[0] 247 | target_embedding_obj = first_encoding["embedding"] 248 | matched_videos_tmp: dict[str, list] = dict() 249 | matched_videos_tmp = self.get_distances( 250 | video, distance_metric, target_embedding_obj 251 | ) 252 | for identity, distances in matched_videos_tmp.items(): 253 | matched_videos[identity] = ( 254 | matched_videos.get(identity, 0) 255 | + sum(distances) / len(distances) 256 | ) / 2 257 | for video_id, scores in matched_videos.items(): 258 | if len(scores) / len(encodings) >= 0.4: 259 | match_count += 1 260 | distance_score = sum(scores) / len(scores) 261 | save_match(video, video_id, distance_score) 262 | video.related_videos.clear() 263 | match_count = 0 264 | 265 | def matching_mode_1( 266 | self, 267 | video: Video, 268 | distance_metric: str = "cosine", 269 | ): 270 | """ 271 | For every detected source face saves all matching values against the target 272 | faces, if they are below the threshold. If 2/5 of the compared faces match 273 | it returns a match. 274 | """ 275 | matched_videos: dict[Video, float] = dict() 276 | target_threshold = ( 277 | settings.RECOGNITION_THRESHOLD 278 | or verification.find_threshold( 279 | settings.RECOGNITION_MODEL, distance_metric 280 | ) 281 | ) 282 | for encoding in tqdm(video.face_encodings): 283 | target_representation = encoding["embedding"] 284 | matched_videos_tmp = self.get_distances( 285 | video, distance_metric, target_representation 286 | ) 287 | 288 | for identity, distances in matched_videos_tmp.items(): 289 | for distance in distances: 290 | if distance <= target_threshold: 291 | matched_videos[identity] = matched_videos.get( 292 | identity, [] 293 | ) + [distance] 294 | 295 | video.related_videos.clear() 296 | match_count = 0 297 | for video_id, scores in sorted( 298 | matched_videos.items(), key=lambda x: x[1] 299 | ): 300 | if sum(scores) / len(scores) <= target_threshold: 301 | match_count += 1 302 | distance_score = sum(scores) / len(scores) 303 | save_match(video, video_id, distance_score) 304 | video.ran_recognition = True 305 | video.save() 306 | print(f"Saved {match_count} matched videos.") 307 | 308 | def get_distances(self, video, distance_metric, target_representation): 309 | matched_videos_tmp: dict[str, list] = dict() 310 | error_counter = 0 311 | for _, db_instance in tqdm(face_database.iterrows()): 312 | if str(db_instance["identity"]) == str(video.id_hash): 313 | continue 314 | source_representation = db_instance["embedding"] 315 | if source_representation is None: 316 | continue 317 | try: 318 | distance = verification.find_distance( 319 | source_representation, 320 | target_representation, 321 | distance_metric, 322 | ) 323 | except ValueError: 324 | error_counter+=1 325 | matched_videos_tmp[db_instance.identity] = matched_videos_tmp.get( 326 | db_instance.identity, [] 327 | ) + [distance] 328 | return matched_videos_tmp 329 | 330 | 331 | def recognize_faces(video: Video): 332 | global face_database 333 | matcher = Matcher() 334 | matcher.start_matching(video) 335 | 336 | 337 | def save_match(video, related_video_id, score): 338 | related_video = Video.objects.filter(id_hash=related_video_id).first() 339 | match = VideoPersonMatch( 340 | source_video=video, 341 | related_video=related_video, 342 | score=score, 343 | ) 344 | match.save() 345 | match_reverse = VideoPersonMatch( 346 | source_video=related_video, 347 | related_video=video, 348 | score=score, 349 | ) 350 | match_reverse.save() 351 | -------------------------------------------------------------------------------- /smol/viewer/views.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pathlib import Path 3 | from typing import Generator 4 | 5 | from django.conf import settings 6 | from django.db import IntegrityError 7 | from django.db.models import Q 8 | from django.db.models.fields import CharField 9 | from django.http import ( 10 | HttpResponse, 11 | JsonResponse, 12 | ) 13 | from django.shortcuts import redirect, render 14 | from django.urls import reverse, reverse_lazy 15 | from django.views import generic 16 | from django.views.decorators.http import require_POST 17 | from django.views.generic.edit import FormMixin, FormView 18 | 19 | from viewer.video_processor import ( 20 | generate_thumbnail, 21 | read_video_info, 22 | recognize_faces, 23 | ) 24 | 25 | from .forms import FilterForm, LabelAddForm, LabelForm 26 | from .models import Image, Label, Video 27 | 28 | 29 | class IndexView(generic.ListView): 30 | template_name = "viewer/index.html" 31 | model = Video 32 | 33 | def get_context_data(self, **kwargs): 34 | context = super().get_context_data(**kwargs) 35 | context["videos"] = Video.objects.order_by("?")[:50] 36 | context["images"] = Image.objects.order_by("?")[:50] 37 | return context 38 | 39 | 40 | class LabelView(FormView, generic.ListView): 41 | template_name = "viewer/labels.html" 42 | form_class = LabelAddForm 43 | success_url = reverse_lazy("viewer:labels") 44 | model = Label 45 | 46 | def post(self, request, *args, **kwargs): 47 | form = self.form_class(request.POST) 48 | context = dict() 49 | if form.is_valid(): 50 | try: 51 | label = request.POST["labels"].lower().strip() 52 | label_obj = Label(label=label) 53 | label_obj.save() 54 | except IntegrityError: 55 | context["error"] = f"Label {label} already exists!" 56 | labels = Label.objects.order_by("label") 57 | context["object_list"] = labels 58 | context["form"] = self.form_class 59 | return render(request, "viewer/labels.html", context) 60 | 61 | def get_context_data(self, **kwargs): 62 | context = super().get_context_data(**kwargs) 63 | context["form"] = self.form_class 64 | context["form"].widget = CharField() 65 | return context 66 | 67 | 68 | def delete_label(request, pk): 69 | if request.method == "GET": 70 | Label.objects.filter(id=pk).delete() 71 | return redirect(reverse("viewer:labels")) 72 | 73 | 74 | def _get_videos(dir: Path) -> Generator[Path, None, None]: 75 | for video in dir.iterdir(): 76 | if video.is_dir() and ".smol" not in video.parts: 77 | yield from _get_videos(video) 78 | elif ( 79 | video.is_file() 80 | and video.suffix in settings.VIDEO_SUFFIXES 81 | and ".smol" not in video.parts 82 | ): 83 | file_path = video.relative_to(settings.MEDIA_ROOT) 84 | if not Video.objects.filter(path=file_path): 85 | print(f"Found {file_path}") 86 | yield file_path 87 | 88 | 89 | def get_new_files(request) -> JsonResponse: 90 | file_paths = list() 91 | for video in _get_videos(settings.MEDIA_DIR): 92 | file_paths.append(str(video)) 93 | response = JsonResponse( 94 | data={"paths": file_paths, "count": len(file_paths)} 95 | ) 96 | return response 97 | 98 | 99 | @require_POST 100 | def load_file(request, *args, **kwargs) -> JsonResponse: 101 | if request.method == "POST": 102 | body = json.loads(request.body) 103 | print("Processing: ", body["path"]) 104 | path = body["path"] 105 | if Video.objects.filter(path=path): 106 | return HttpResponse("Document already imported!", status=409) 107 | video_path = settings.MEDIA_DIR / path 108 | video_data = read_video_info(video_path) 109 | relative_video_path = video_path.relative_to(settings.MEDIA_ROOT) 110 | video_data["size"] = video_path.stat().st_size 111 | video_data["path"] = relative_video_path 112 | video_data["filename"] = video_path.name 113 | video_obj = Video(**video_data) 114 | video_obj.thumbnail = generate_thumbnail(video_obj, video_path) 115 | video_obj.save(force_insert=True) 116 | return JsonResponse( 117 | { 118 | "file": body["path"], 119 | "id": video_obj.id, 120 | "thumbnail": video_obj.thumbnail, 121 | } 122 | ) 123 | 124 | 125 | def add_video_label(request): 126 | if request.method == "POST": 127 | post_data = json.loads(request.body) 128 | video_id = post_data["video_id"] 129 | labels = post_data["labels"] 130 | label_objs = Label.objects.filter(id__in=labels).all() 131 | video_obj = Video.objects.filter(id=video_id).first() 132 | video_obj.labels.clear() 133 | [video_obj.labels.add(label_obj) for label_obj in label_objs] 134 | video_obj.save() 135 | return JsonResponse( 136 | {"labels": list(video_obj.labels.values("label").all())} 137 | ) 138 | 139 | 140 | def delete_video_label(request): 141 | if request.method == "POST": 142 | video_id = request.POST["video_id"] 143 | label_id = request.POST["label_id"] 144 | label_obj = Label.objects.filter(id=label_id).first() 145 | video_obj = Video.objects.filter(id=video_id).first() 146 | video_obj.labels.remove(label_obj) 147 | return HttpResponse("OK") 148 | 149 | 150 | class DataLoader(generic.ListView): 151 | template_name = "viewer/loader.html" 152 | model = Video 153 | 154 | def get_context_data(self, **kwargs): 155 | context = super(DataLoader, self).get_context_data(**kwargs) 156 | context["video_count"] = self.get_queryset().count() 157 | context["image_count"] = Image.objects.all().count() 158 | return context 159 | 160 | 161 | class VideoView(FormMixin, generic.DetailView): 162 | model = Video 163 | template_name = "viewer/video.html" 164 | form_class = LabelForm 165 | 166 | def get_context_data(self, **kwargs): 167 | context = super(generic.DetailView, self).get_context_data(**kwargs) 168 | video = context["object"] 169 | context["video"] = video 170 | context["form"] = self.form_class( 171 | initial={ 172 | "labels": video.labels.all(), 173 | } 174 | ) 175 | context["recommendations"] = ( 176 | Video.objects.filter(labels__in=video.labels.all()) 177 | .exclude(id=video.id) 178 | .order_by("?") 179 | .distinct()[:50] 180 | ) 181 | return context 182 | 183 | 184 | class VideoList(generic.ListView): 185 | form_class = FilterForm 186 | template_name = "viewer/videos_page.html" 187 | model = Video 188 | paginate_by = 30 189 | context_object_name = "videos" 190 | initial = {"order_videos": 1} 191 | 192 | def get_queryset(self): 193 | order = self.request.GET.get("order_videos") 194 | labels = self.request.GET.getlist("label_field") 195 | queryset = Video.objects.all() 196 | if labels: 197 | queryset = queryset.filter(labels__in=labels) 198 | if order: 199 | queryset = queryset.order_by(order) 200 | print(queryset.query) 201 | return queryset 202 | 203 | def get_context_data(self, **kwargs): 204 | context = super(VideoList, self).get_context_data(**kwargs) 205 | context["form"] = self.form_class( 206 | initial={ 207 | "order_videos": self.request.GET.get("order_videos", 1), 208 | "label_field": self.request.GET.getlist("label_field", []), 209 | } 210 | ) 211 | return context 212 | 213 | 214 | class SearchView(VideoList): 215 | template_name = "viewer/index.html" 216 | 217 | def get_queryset(self): 218 | search_query = self.request.GET["query"] 219 | title = Video.objects.filter(labels__label__in=search_query) 220 | return title 221 | 222 | def get_context_data(self, **kwargs): 223 | context = super().get_context_data(**kwargs) 224 | search_query = self.request.GET["query"] 225 | context["videos"] = Video.objects.filter( 226 | Q(filename__icontains=search_query) 227 | | Q(path__icontains=search_query) 228 | ) 229 | context["images"] = Image.objects.filter( 230 | Q(filename__icontains=search_query) 231 | | Q(path__icontains=search_query) 232 | ) 233 | return context 234 | 235 | 236 | def clean_data(request): 237 | counter = {"videos": 0, "images": 0} 238 | for video in Video.objects.all(): 239 | counter["videos"] += video.clean() 240 | for image in Image.objects.all(): 241 | counter["images"] += image.clean() 242 | return JsonResponse(counter) 243 | 244 | 245 | class LabelResultView(generic.DetailView): 246 | model = Label 247 | template_name = "viewer/index.html" 248 | 249 | def get_context_data(self, **kwargs): 250 | context = super(generic.DetailView, self).get_context_data(**kwargs) 251 | label_obj = context["object"] 252 | context["videos"] = Video.objects.filter(labels=label_obj).order_by( 253 | "-favorite" 254 | )[:50] 255 | context["images"] = Image.objects.filter(labels=label_obj).order_by( 256 | "-favorite" 257 | )[:50] 258 | print(context) 259 | return context 260 | 261 | 262 | def add_favorite(request, videoid): 263 | vid_obj = Video.objects.get(id=videoid) 264 | vid_obj.favorite = True 265 | vid_obj.save() 266 | return JsonResponse({"id": videoid, "status": True}) 267 | 268 | 269 | def rem_favorite(request, videoid): 270 | vid_obj = Video.objects.get(id=videoid) 271 | vid_obj.favorite = False 272 | vid_obj.save() 273 | return JsonResponse({"id": videoid, "status": False}) 274 | 275 | 276 | def add_favorite_image(request, imageid): 277 | imageid = int(imageid) 278 | img_obj = Image.objects.get(id=imageid) 279 | img_obj.favorite = True 280 | img_obj.save() 281 | return JsonResponse({"id": imageid, "status": True}) 282 | 283 | 284 | def rem_favorite_image(request, imageid): 285 | imageid = int(imageid) 286 | img_obj = Image.objects.get(id=imageid) 287 | img_obj.favorite = False 288 | img_obj.save() 289 | return JsonResponse({"id": imageid, "status": False}) 290 | 291 | 292 | def rem_video(request): 293 | if request.method == "POST": 294 | body = json.loads(request.body) 295 | video_id = body["video_id"] 296 | vid_obj: Video = Video.objects.filter(id=video_id).first() 297 | vid_obj.delete_full() 298 | return HttpResponse("OK") 299 | 300 | 301 | def scan_video(request): 302 | if request.method == "POST": 303 | body = json.loads(request.body) 304 | video_id = body["video_id"] 305 | video = Video.objects.filter(id=video_id).first() 306 | percent = recognize_faces(video) 307 | return HttpResponse(percent) 308 | 309 | 310 | def delete_encoding(request): 311 | if request.method == "POST": 312 | body = json.loads(request.body) 313 | video_id = body["video_id"] 314 | video = Video.objects.filter(id=video_id).first() 315 | video.delete_encoding() 316 | return HttpResponse("OK") 317 | 318 | 319 | def rem_meta(request): 320 | if request.method == "POST": 321 | body = json.loads(request.body) 322 | video_id = body["video_id"] 323 | vid_obj: Video = Video.objects.filter(id=video_id).first() 324 | vid_obj.delete_entry() 325 | return HttpResponse("OK") 326 | 327 | 328 | def rem_image(request): 329 | if request.method == "POST": 330 | image_id = request.POST["image_id"] 331 | img_obj = Image.objects.filter(id=image_id).first() 332 | img_obj.delete_full() 333 | return HttpResponse("OK") 334 | --------------------------------------------------------------------------------