Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions demo/.env
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ APP_SECRET=ccb9dca72dce53c683eaaf775bfdb253
CHROMADB_HOST=chromadb
CHROMADB_PORT=8080
OPENAI_API_KEY=sk-...
HUGGINGFACE_API_KEY=hf-...
1 change: 1 addition & 0 deletions demo/assets/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'bootstrap/dist/css/bootstrap.min.css';
import './styles/app.css';
import './styles/audio.css';
import './styles/blog.css';
import './styles/crop.css';
import './styles/stream.css';
import './styles/youtube.css';
import './styles/video.css';
Expand Down
9 changes: 9 additions & 0 deletions demo/assets/controllers.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
{
"controllers": {
"@symfony/ux-dropzone": {
"dropzone": {
"enabled": true,
"fetch": "eager",
"autoimport": {
"@symfony/ux-dropzone/dist/style.min.css": true
}
}
},
"@symfony/ux-live-component": {
"live": {
"enabled": true,
Expand Down
33 changes: 33 additions & 0 deletions demo/assets/controllers/dropzone_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Controller } from '@hotwired/stimulus';

export default class extends Controller {
connect() {
this.element.addEventListener('dropzone:change', this._onChange.bind(this));
this.element.addEventListener('dropzone:clear', this._onClear);
}

disconnect() {
this.element.removeEventListener('dropzone:change', this._onChange.bind(this));
this.element.removeEventListener('dropzone:clear', this._onClear);
}

async _onChange(event) {
const cropComponent = document.getElementById('crop-component').__component;

cropComponent.set('imageData', await this.blobToBase64(event.detail));
}

_onClear(event) {
const cropComponent = document.getElementById('crop-component').__component;

cropComponent.set('imageData', null);
}

blobToBase64(blob) {
return new Promise((resolve) => {
const reader = new FileReader();
reader.readAsDataURL(blob);
reader.onloadend = () => resolve(reader.result);
});
}
}
1 change: 1 addition & 0 deletions demo/assets/icons/material-symbols/crop.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 18 additions & 0 deletions demo/assets/styles/crop.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
.crop {
body&, .card-img-top {
background: rgb(34,34,34);
background: linear-gradient(0deg, rgb(209, 14, 205) 0%, rgb(120, 21, 135) 100%);
}

.card-img-top {
color: #ffffff;
}

& footer {
color: #ffffff;

a {
color: #ffffff;
}
}
}
4 changes: 4 additions & 0 deletions demo/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"php": ">=8.4",
"ext-ctype": "*",
"ext-iconv": "*",
"ext-gd": "*",
"codewithkyrian/chromadb-php": "^0.4.0",
"league/commonmark": "^2.7",
"mcp/sdk": "@dev",
Expand All @@ -22,13 +23,16 @@
"symfony/dom-crawler": "~7.3.0",
"symfony/dotenv": "~7.3.0",
"symfony/flex": "^2.5",
"symfony/form": "~7.3.0",
"symfony/framework-bundle": "~7.3.0",
"symfony/http-client": "~7.3.0",
"symfony/mcp-bundle": "@dev",
"symfony/mime": "~7.3.0",
"symfony/monolog-bundle": "^3.10",
"symfony/runtime": "~7.3.0",
"symfony/twig-bundle": "~7.3.0",
"symfony/uid": "~7.3.0",
"symfony/ux-dropzone": "^2.31",
"symfony/ux-icons": "^2.25",
"symfony/ux-live-component": "^2.25",
"symfony/ux-turbo": "^2.25",
Expand Down
1 change: 1 addition & 0 deletions demo/config/bundles.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
Symfony\UX\Dropzone\DropzoneBundle::class => ['all' => true],
Symfony\UX\Icons\UXIconsBundle::class => ['all' => true],
Symfony\UX\LiveComponent\LiveComponentBundle::class => ['all' => true],
Symfony\UX\StimulusBundle\StimulusBundle::class => ['all' => true],
Expand Down
11 changes: 11 additions & 0 deletions demo/config/packages/ai.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ ai:
platform:
openai:
api_key: '%env(OPENAI_API_KEY)%'
huggingface:
api_key: '%env(HUGGINGFACE_API_KEY)%'
agent:
blog:
platform: 'ai.platform.openai'
model: 'gpt-4o-mini'
tools:
- 'Symfony\AI\Agent\Toolbox\Tool\SimilaritySearch'
Expand All @@ -12,14 +15,17 @@ ai:
description: 'Provides the current date and time.'
method: 'now'
stream:
platform: 'ai.platform.openai'
model: 'gpt-4o-mini'
prompt:
file: '%kernel.project_dir%/prompts/stream-chat.txt'
tools: false
youtube:
platform: 'ai.platform.openai'
model: 'gpt-4o-mini'
tools: false
wikipedia:
platform: 'ai.platform.openai'
model:
name: 'gpt-4o-mini'
options:
Expand All @@ -31,6 +37,7 @@ ai:
- 'Symfony\AI\Agent\Toolbox\Tool\Wikipedia'
include_sources: true
audio:
platform: 'ai.platform.openai'
model: 'gpt-4o-mini?temperature=1.0'
prompt: 'You are a friendly chatbot that likes to have a conversation with users and asks them some questions.'
tools:
Expand All @@ -39,14 +46,17 @@ ai:
name: 'symfony_blog'
description: 'Can answer questions based on the Symfony blog.'
orchestrator:
platform: 'ai.platform.openai'
model: 'gpt-4o-mini'
prompt: 'You are an intelligent agent orchestrator that routes user questions to specialized agents.'
tools: false
technical:
platform: 'ai.platform.openai'
model: 'gpt-4o-mini'
prompt: 'You are a technical support specialist. Help users resolve bugs, problems, and technical errors.'
tools: false
fallback:
platform: 'ai.platform.openai'
model: 'gpt-4o-mini'
prompt: 'You are a helpful general assistant. Assist users with any questions or tasks they may have.'
tools: false
Expand All @@ -62,6 +72,7 @@ ai:
collection: 'symfony_blog'
vectorizer:
openai:
platform: 'ai.platform.openai'
model: 'text-embedding-ada-002'
indexer:
blog:
Expand Down
11 changes: 11 additions & 0 deletions demo/config/packages/csrf.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Enable stateless CSRF protection for forms and logins/logouts
framework:
form:
csrf_protection:
token_id: submit

csrf_protection:
stateless_token_ids:
- submit
- authenticate
- logout
1 change: 1 addition & 0 deletions demo/config/packages/twig.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
twig:
file_name_pattern: '*.twig'
form_themes: [ 'bootstrap_5_layout.html.twig' ]

when@test:
twig:
Expand Down
6 changes: 6 additions & 0 deletions demo/config/routes.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ blog:
template: 'chat.html.twig'
context: { chat: 'blog' }

crop:
path: '/crop'
controller: 'Symfony\Bundle\FrameworkBundle\Controller\TemplateController'
defaults:
template: 'crop.html.twig'

stream:
path: '/stream'
controller: 'Symfony\Bundle\FrameworkBundle\Controller\TemplateController'
Expand Down
1 change: 1 addition & 0 deletions demo/src/Audio/Chat.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ final class Chat
private const SESSION_KEY = 'audio-chat';

public function __construct(
#[Autowire(service: 'ai.platform.openai')]
private readonly PlatformInterface $platform,
private readonly RequestStack $requestStack,
#[Autowire(service: 'ai.agent.audio')]
Expand Down
56 changes: 56 additions & 0 deletions demo/src/Crop/CropForm.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace App\Crop;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\UX\Dropzone\Form\DropzoneType;

final class CropForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('originalImage', DropzoneType::class, [
'label' => 'Image to crop',
'attr' => [
'data-controller' => 'dropzone',
'placeholder' => 'Drag and drop an image or click to browse',
],
])
->add('format', ChoiceType::class, [
'choices' => [
'Square (1:1)' => '1:1',
'Landscape (16:9)' => '16:9',
'Portrait (9:16)' => '9:16',
],
'expanded' => true,
'multiple' => false,
])
->add('width', ChoiceType::class, [
'choices' => [
'Small (400px)' => 400,
'Medium (800px)' => 800,
'Large (1200px)' => 1200,
],
'expanded' => true,
'multiple' => false,
])
;
}

public function getBlockPrefix(): string
{
return '';
}
}
63 changes: 63 additions & 0 deletions demo/src/Crop/Image/Analyzer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace App\Crop\Image;

use Symfony\AI\Platform\Bridge\HuggingFace\Output\ObjectDetectionResult;
use Symfony\AI\Platform\Bridge\HuggingFace\Task;
use Symfony\AI\Platform\Message\Content\Image;
use Symfony\AI\Platform\PlatformInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;

final readonly class Analyzer
{
public function __construct(
#[Autowire(service: 'ai.platform.huggingface')]
private PlatformInterface $platform,
) {
}

public function getRelevantArea(string $imageData): RelevantArea
{
$result = $this->platform->invoke('facebook/detr-resnet-50', Image::fromDataUrl($imageData), [
'task' => Task::OBJECT_DETECTION,
])->asObject();

\assert($result instanceof ObjectDetectionResult);

if ([] === $result->objects) {
throw new \RuntimeException('No objects detected.');
}

$init = $result->objects[0];
$xMin = $init->xmin;
$yMin = $init->ymin;
$xMax = $init->xmax;
$yMax = $init->ymax;

foreach ($result->objects as $object) {
if ($object->xmin < $xMin) {
$xMin = $object->xmin;
}
if ($object->ymin < $yMin) {
$yMin = $object->ymin;
}
if ($object->xmax > $xMax) {
$xMax = $object->xmax;
}
if ($object->ymax > $yMax) {
$yMax = $object->ymax;
}
}

return new RelevantArea((int) $xMin, (int)$yMin, (int)$xMax, (int)$yMax);
}
}
51 changes: 51 additions & 0 deletions demo/src/Crop/Image/RelevantArea.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace App\Crop\Image;

final readonly class RelevantArea
{
public function __construct(
public int $xMin,
public int $yMin,
public int $xMax,
public int $yMax,
) {
}

/**
* @return int<1, max>
*/
public function getWidth(): int
{
$width = $this->xMax - $this->xMin;

if ($width < 1) {
throw new \InvalidArgumentException('Width must be at least 1 pixel.');
}

return $width;
}

/**
* @return int<1, max>
*/
public function getHeight(): int
{
$height = $this->yMax - $this->yMin;

if ($height < 1) {
throw new \InvalidArgumentException('Height must be at least 1 pixel.');
}

return $height;
}
}
Loading