Skip to content
Open
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
25 changes: 25 additions & 0 deletions app/_config/voice.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
Name: voice-briefings
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are two new config files (voice.yml and voicebriefings.yml) configuring the same classes with different Name headers and different class-name casing. Keeping both is likely to cause confusing/duplicate config merges and make it unclear which values are effective. Please consolidate into a single config file and ensure class keys match the actual namespaces used in code.

Suggested change
Name: voice-briefings
Name: voice

Copilot uses AI. Check for mistakes.
---

App\Services\VoiceBriefingService:
provider: 'elevenlabs'
voice: 'nova'
speed: 1.0
format: 'mp3'
cache_ttl: 86400
audio_directory: 'assets/voice-briefings'
quiet_hours:
start: '22:00'
end: '07:00'

App\Model\VoiceBriefing:
cache_ttl: 86400

App\Tasks\VoiceBriefingTask:
schedule:
morning: '07:00'
meeting: '09:00'
evening: '17:00'
urgent: 'immediate'
send_delivery: true
25 changes: 25 additions & 0 deletions app/_config/voicebriefings.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
Name: voicebriefings
---

App\model\VoiceBriefing:
cache_ttl: 86400

App\services\VoiceBriefingService:
provider: 'elevenlabs'
Comment on lines +5 to +9
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Config keys here use App\\model / App\\services (lowercase segments) while other code/config uses App\\Model / App\\Services. SilverStripe config is keyed by the exact class name string, so mixed casing can lead to config not applying. Align these keys with the canonical namespaces used in code (and remove the duplicate config file).

Copilot uses AI. Check for mistakes.
voice: 'nova'
speed: 1.0
format: 'mp3'
cache_ttl: 86400
audio_directory: 'assets/voice-briefings'
quiet_hours:
start: '22:00'
end: '07:00'

App\tasks\VoiceBriefingTask:
schedule:
morning: '07:00'
meeting: '09:00'
evening: '17:00'
urgent: 'immediate'
send_delivery: true
147 changes: 147 additions & 0 deletions app/src/Model/VoiceBriefing.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
<?php

namespace App\model;
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Namespace casing here (App\model) doesn’t match other DataObjects (e.g. namespace App\Model; in app/src/Model/Product.php) and conflicts with app/_config/voice.yml which configures App\Model\VoiceBriefing. Please standardise the namespace to App\Model (and update all references/config keys).

Suggested change
namespace App\model;
namespace App\Model;

Copilot uses AI. Check for mistakes.

use SilverStripe\ORM\DataObject;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\TextField;
use SilverStripe\Forms\DropdownField;
use SilverStripe\Forms\CheckboxField;
use SilverStripe\Forms\TextareaField;

/**
* VoiceBriefing - DataObject for tracking generated voice briefings
*
* @property string $Type morning|meeting|evening|urgent
* @property string $AudioPath Path to generated audio file
* @property string $DeliveryMethod telegram|email|podcast
* @property bool $Delivered Whether the briefing has been delivered
* @property string $Transcript Text transcript of the briefing
* @property string $ScriptHash Hash of the script for caching
* @property string $ContentSummary Summary of briefing content
* @property int $DurationSeconds Length of audio in seconds
* @property string $RecipientID Member ID or email/telegram ID
* @property string $ErrorMessage Any error during generation/delivery
*/
class VoiceBriefing extends DataObject
{
private static $table_name = 'VoiceBriefing';

private static $db = [
'Type' => "Enum('morning,meeting,evening,urgent', 'morning')",
'AudioPath' => 'Varchar(255)',
'DeliveryMethod' => "Enum('telegram,email,podcast', 'telegram')",
'Delivered' => 'Boolean',
'Transcript' => 'Text',
'ScriptHash' => 'Varchar(64)',
'ContentSummary' => 'Text',
'DurationSeconds' => 'Int',
'RecipientID' => 'Varchar(255)',
'ErrorMessage' => 'Text',
];

private static $defaults = [
'Delivered' => false,
];

private static $default_sort = 'Created DESC';

private static $summary_fields = [
'Type' => 'Type',
'Created.Nice' => 'Generated',
'DeliveryMethod' => 'Delivery',
'Delivered.Nice' => 'Delivered',
'DurationSeconds' => 'Duration (s)',
'ContentSummary' => 'Summary',
];

private static $searchable_fields = [
'Type',
'DeliveryMethod',
'Delivered',
'ContentSummary',
];

public function getCMSFields()
{
$fields = parent::getCMSFields();

$fields->addFieldsToTab('Root.Main', [
DropdownField::create('Type', 'Briefing Type', [
'morning' => 'Morning Briefing',
'meeting' => 'Meeting Reminder',
'evening' => 'End-of-Day Summary',
'urgent' => 'Urgent Alert',
]),
DropdownField::create('DeliveryMethod', 'Delivery Method', [
'telegram' => 'Telegram',
'email' => 'Email',
'podcast' => 'Podcast Feed',
]),
TextField::create('RecipientID', 'Recipient ID'),
TextField::create('AudioPath', 'Audio File Path'),
TextareaField::create('Transcript', 'Transcript'),
TextareaField::create('ContentSummary', 'Content Summary'),
CheckboxField::create('Delivered', 'Delivered'),
]);

return $fields;
}

/**
* Get audio file URL
*/
public function getAudioUrl(): ?string
{
if (!$this->AudioPath) {
return null;
}
return '/' . ltrim($this->AudioPath, '/');
}

/**
* Get formatted duration (m:ss)
*/
public function getFormattedDuration(): string
{
if (!$this->DurationSeconds) {
return '0:00';
}
$minutes = (int) floor($this->DurationSeconds / 60);
$seconds = $this->DurationSeconds % 60;
return sprintf('%d:%02d', $minutes, $seconds);
}

/**
* Get type label
*/
public function getTypeLabel(): string
{
$labels = [
'morning' => 'Morning Briefing',
'meeting' => 'Meeting Reminder',
'evening' => 'End-of-Day Summary',
'urgent' => 'Urgent Alert',
];
return $labels[$this->Type] ?? $this->Type;
}

/**
* Clean up expired briefings
*/
public static function cleanupExpired(): int
{
$ttl = self::config()->get('cache_ttl') ?: 86400;
$cutoff = date('Y-m-d H:i:s', time() - $ttl);
$expired = self::get()->filter('Created:LessThan', $cutoff);
$count = $expired->count();

foreach ($expired as $briefing) {
if ($briefing->AudioPath && file_exists(BASE_PATH . '/' . ltrim($briefing->AudioPath, '/'))) {
unlink(BASE_PATH . '/' . ltrim($briefing->AudioPath, '/'));
}
$briefing->delete();
}
return $count;
}
}
Loading